Your app works locally. You can create an account, click through it and show a demo. Production work begins when the right people need access to the right data, even when a request fails.

Whether the code came from Codex, Grok or you does not change the questions to ask about the server. Who can administer it? Where are the secrets? How do you return to the previous version? Here is a starting point for a VPS deployment, not a security certification.

Expose the site, not the whole machine

In this example, Caddy runs on the host and receives web traffic. The Docker app is published only on the server’s loopback address. It listens on 0.0.0.0:3000 inside the container. The image must already support running without privileges and without writing outside designated locations.

APP_IMAGE must contain a verified image reference, ideally pinned by digest. User 10001 must match your image’s user. Adjust memory limits after measuring usage. Do not put secrets in this file or bake them into the image.

compose.yaml
services:
  app:
    image: ${APP_IMAGE:?Set a verified image reference}
    user: "10001:10001"
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000"
    read_only: true
    tmpfs:
      - /tmp:size=64m,mode=1777
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    pids_limit: 100
    mem_limit: 512m
    cpus: 1.0
Example for a stateless app, not a complete deployment

Connect the domain and check from outside

Replace app.example.com with your domain and point its DNS to the VPS. For Caddy’s standard HTTPS setup, ports 80 and 443 need to be reachable and its data directory must remain persistent. SSH administration stays restricted to the intended access paths.

Caddyfile
app.example.com {
  reverse_proxy 127.0.0.1:3000
}
Caddy on the host, in front of the Docker app

Prepare for failure before launch

Use a patched Docker version and test ports from another machine. Its documentation notes a loopback port isolation limitation before version 28.0.0. Published ports can also bypass UFW rules. An “active firewall” status is not enough.

Before opening registration, check authorization with two separate accounts, limits on public endpoints and restoration from an off-server backup. Keep the previous image and plan for migration rollback. Docker and HTTPS do not fix an endpoint that lets someone read another account’s data.