"It works in Docker on my laptop" is a great start. Getting that container onto a real server, behind a domain, with HTTPS and a way back when an update breaks, is where many small teams stall. Usually because nobody on the team is "the DevOps person".
You don't need one for a single server. This guide takes a Dockerfile or Compose project from your repo to a live HTTPS URL in six steps, explains the parts people get wrong, and then shows how ShipOwn automates the same path from a desktop app.
Why Docker on a VPS at all?
- Any language. Python, Go, Rust, PHP, Ruby, Java and .NET apps all run the same way once they're in a container.
- Same environment everywhere. The image you test is the image you run.
- Clean server. Dependencies live in the image, not on the host.
If your app is plain Node.js, you may not need Docker at all; PM2 is lighter. Docker earns its place when you have non-Node services or several services wired together with Compose.
Step 1: Install Docker properly
On Ubuntu, install Docker Engine and the Compose plugin from Docker's official repository (or with the convenience script for a quick test):
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER # log out and in again
docker compose version
Step 2: Build on the server
git clone https://github.com/you/service.git && cd service
docker compose build
Tag images with the commit you built from, so you always know what's running:
docker build -t service:$(git rev-parse --short HEAD) .
Step 3: Bind to localhost, not the world
This is the most common Docker-on-VPS mistake. ports: "8000:8000" publishes the port on every interface, and Docker's own firewall rules can bypass ufw. Bind to loopback instead and let Nginx be the only public entry:
services:
web:
build: .
restart: unless-stopped
ports:
- "127.0.0.1:8000:8000"
env_file: .env
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
retries: 3
Step 4: Start it and check health
docker compose up -d
docker compose ps # look for "healthy"
docker compose logs -f web
restart: unless-stopped brings the container back after a crash or reboot.
Step 5: Nginx and HTTPS
server {
server_name service.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d service.example.com
Step 6: Update safely and keep a way back
The risky update is git pull && docker compose up -d --build on a live service. If the new image starts but is broken, you're down until you fix it.
A safer pattern:
- Build the new image with a new tag.
- Start it alongside the old one on a different localhost port.
- Wait for its health check to pass.
- Switch Nginx to the new port,
nginx -t, reload. - Keep the old image for a quick rollback.
It works, but it's a lot of steps to repeat by hand.
Doing it with ShipOwn
ShipOwn is a desktop app for Windows, macOS and Linux that deploys over plain SSH. No ShipOwn deployment agent is installed on the server. For Docker apps it follows the safe pattern above:
- Builds your Dockerfile or Compose file on the server.
- Labels images with the commit they came from.
- Routes through Nginx to
127.0.0.1when configured. - Keeps the previous container until the health check passes.
- Lets you go back to an earlier image later.
The deploy flow is the same as for every ShipOwn app: connect a server, pick a GitHub branch or local folder, review an editable plan, watch the build stream live, then operate it from one page. If the server has no Docker yet, ShipOwn can install it for you.
Managing containers you already have
ShipOwn also works with containers already running on the server. It inspects every container, maps published and internal ports, shows CPU and memory gauges, calls out crash loops, and gives you start, stop and restart, a shell inside the container, and its logs.
Domains and certificates
Point a domain at the app and ShipOwn writes the Nginx virtual host, tests it before reloading, restores it if Nginx rejects it, and asks Certbot for a certificate. Certificate expiry is shown in the app.
Troubleshooting checklist
| Symptom | Likely cause |
|---|---|
| Container restarts in a loop | App crashes on start; read docker logs
|
| 502 from Nginx | Wrong port, or container bound to a different interface |
Service reachable on IP:8000 from the internet |
Port published on 0.0.0.0 instead of 127.0.0.1
|
| Disk full | Old images and build cache; docker system prune with care |
| Env vars missing |
.env not loaded or wrong env_file path |
Stuck on something else? ShipOwn's AI terminal uses your own Anthropic, OpenAI or DeepSeek key to propose one command at a time, with its reason. You approve each one, and destructive commands always wait for confirmation.
FAQ
Does ShipOwn use Kubernetes or Swarm? No. It builds and runs containers on a single Linux server you control.
Is Docker required for every app? No. Node apps run under PM2 and static sites are served by Nginx without Docker.
Is ShipOwn open source? No, it's a commercial app. What runs on your server is plain SSH and shell commands.
Conclusion
You don't need a DevOps engineer to run containers on a VPS. You need localhost-bound ports, Nginx, HTTPS, health checks and a rollback plan. Learn the steps above, then let ShipOwn run them for you. Free 30-day trial, no card.
Written by Vishvajeet Shukla, full stack developer and creator of ShipOwn.
Top comments (0)