This guide shows you how to deploy a Web app to a Server with Docker, Docker Compose, Caddy, and a custom domain. It starts from an Ubuntu-based server with Docker installed; follow the Linux server setup guide first if your server is not ready.
You will first complete the deployment manually so you can understand each moving part. If you only want the automated workflow, jump to the QuickDeploy section.
Manual VPS deployments have several failure points. The guide to what actually breaks during VPS deployments explains the most common ones and how to avoid them.
- Create a Dockerfile for your Web app
- Build the Docker image and upload it to your server
- Run your app using Docker Compose
- Add a reverse proxy, domain, and HTTPS
- The easy way: QuickDeploy
Create a Dockerfile for Your Web App
In the root of your project, create a file named Dockerfile. It defines the base image, installs dependencies, builds the application, and starts the production server.
This general guide uses a Node.js application that listens on port 8080. Adjust the Dockerfile, port, and start command for your application.
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 8080
ENV PORT=8080
CMD ["npm", "start"]
Build the Docker Image and Upload It to Your Server
Build the image from the directory containing the Dockerfile:
docker build -t webapp:latest .
Run it locally to confirm that the production process starts and responds on port 8080:
docker run --rm -p 8080:8080 webapp:latest
Open http://localhost:8080. If the application works, transfer the image directly to the server over SSH:
docker save webapp:latest | ssh user@your-vps-ip 'docker load'
Replace user@your-vps-ip with your SSH user and server address. A container registry is another option, but this direct transfer keeps the example self-contained.
Run Your App Using Docker Compose
Connect to the server and create a shared Docker network. The application and Caddy reverse proxy will use this network to communicate:
docker network create web
The command reports an error if the network already exists; you can safely continue in that case. Create ~/webapp/compose.yml with this configuration:
services:
webapp:
container_name: webapp
image: webapp:latest
expose:
- "8080"
networks:
- web
restart: unless-stopped
networks:
web:
external: true
Start the application:
cd ~/webapp
docker compose up -d
Your application is now running inside Docker, but it is not yet publicly accessible.
Add a Reverse Proxy, Domain, and HTTPS
Before continuing, create an A record that points your domain to the server's public IP address. Then create ~/caddy/compose.yml and ~/caddy/Caddyfile.
The Caddyfile maps your domain to the application container:
webapp.com {
reverse_proxy webapp:8080
}
The Compose file starts Caddy, publishes ports 80 and 443, and joins the existing web network:
services:
caddy:
container_name: caddy
image: caddy:latest
ports:
- '80:80'
- '443:443'
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./data:/data
- ./config:/config
networks:
- web
restart: unless-stopped
networks:
web:
external: true
Start Caddy:
cd ~/caddy
docker compose up -d
Open your domain over HTTPS to verify the deployment. Caddy obtains and renews the TLS certificate automatically. To ship an update manually, rebuild and transfer the image, then recreate the application container.
The Easy Way: QuickDeploy
QuickDeploy automates the image build, transfer, Compose configuration, Caddy routing, HTTPS, and update workflow shown above. From your project directory, deploy with:
quickdeploy push --domain webapp.com
It also performs rolling application updates so the existing container remains available while the replacement starts. QuickDeploy runs from your computer and deploys to an Ubuntu-based VPS you control.
Get QuickDeploy and skip the manual deployment checklist.
Deploying Next.js with Prisma and a database too? Follow the Next.js, Prisma, and PostgreSQL VPS deployment guide for Docker networking, persistent database storage, production migrations, and HTTPS setup.