Skip to content
Docs

Deploy Go apps on Vercel using Docker

Deploy an existing Dockerized Go app to Vercel using Memos as a real-world example, with Neon Postgres for durable data.

Anshuman BhardwajContent Engineer

Go apps usually deploy cleanly on Vercel with the native Go preset: a go.mod, a supported entrypoint, and an HTTP server that listens on PORT. But not every Go project fits that shape. Some ship their own startup scripts, depend on external binaries, or are already published as a container image you'd rather run as-is.

Docker often fits those cases better. This guide shows how to deploy a Dockerized Go application on Vercel using Memos, an open-source note-taking app, as an example. Memos runs as a Go server from the neosmemo/memos:stable image and stores durable data in Postgres.

Copy link to headingWhen to run Go using Docker

Use Vercel's native Go Runtime when your app is a standard Go HTTP server with a root go.mod, a supported entrypoint like main.go, and code you can adapt to listen on PORT.

Use Docker when the app:

  • Already ships as a maintained OCI image.
  • Has a custom entrypoint or runtime behavior you do not want to rewrite.
  • Needs operating-system packages, CA bundles, native binaries, or bundled assets.
  • Uses a framework or app layout outside Vercel's Go preset assumptions.
  • Should run locally and in production from the same image.
  • Is an upstream open-source app, like Memos, where you want to configure rather than fork.

Docker on Vercel still runs as a Vercel Function. The container must serve HTTP, stay stateless, use external storage for durable data, and respect Function limits. Vercel builds the image, stores it in Vercel Container Registry, and routes traffic to it.

Copy link to headingHow to run Docker on Vercel

Vercel supports container images through Dockerfile.vercel or Containerfile.vercel. In a Services project, declare the container service in vercel.json and expose it with a rewrite.

Memos is a useful example because it is a Go app, already has an official Docker image, and can switch from local SQLite to Postgres through environment variables.

Copy link to headingAdd Vercel Dockerfile

First, create a Dockerfile.vercel that only runs the upstream Memos image on a port that Vercel can reach:

Dockerfile.vercel
FROM neosmemo/memos:stable
ENV MEMOS_ADDR=0.0.0.0 \
MEMOS_PORT=5230 \
MEMOS_DATA=/tmp/memos
RUN mkdir -p /tmp/memos && chmod 1777 /tmp/memos
EXPOSE 5230

Here is what each part does:

  • FROM neosmemo/memos:stable uses the official Memos image instead of rebuilding the Go app from source.
  • MEMOS_ADDR=0.0.0.0 makes the server accept traffic from outside the container.
  • MEMOS_PORT=5230 keeps Memos on its normal unprivileged port. The image runs as a non-root user, so binding to port 80 can fail.
  • MEMOS_DATA=/tmp/memos gives Memos a writable local directory. On Vercel this is ephemeral, so it must not be the system of record.
  • EXPOSE 5230 documents the container port.

Also, set the Vercel project environment variable PORT=5230. Vercel routes to port 80 by default unless PORT is configured. Memos itself reads MEMOS_PORT, while Vercel's container router reads PORT, so both values matter.

Terminal
vercel env add PORT production --no-sensitive
vercel env add PORT preview --no-sensitive
vercel env add PORT development

Copy link to headingAdd Postgres from the Vercel Marketplace

The Memos app requires a persistent database service. SQLite is not suitable for durable production data on Vercel Functions. Use a Postgres database integration such as Neon instead. This will provide the data and file storage for the app.

From a linked Vercel project, you can discover and install Neon from the Marketplace with the Vercel CLI.

Install Neon, create a resource, and connect it to the current project:

Terminal
vercel integration add neon

Copy link to headingConnect Memos to Postgres

Memos needs these settings to use Postgres:

Dockerfile.vercel
MEMOS_DRIVER=postgres

Add MEMOS_DRIVER=postgres to the Dockerfile:

Dockerfile.vercel
ENV MEMOS_ADDR=0.0.0.0 \
MEMOS_PORT=5230 \
MEMOS_DRIVER=postgres \
MEMOS_DATA=/tmp/memos

You could manually copy Neon's connection string into a new Vercel environment variable named MEMOS_DSN, but that duplicates a secret. To avoid duplicating the secret, derive MEMOS_DSN at startup.

Add this ENTRYPOINT line to Dockerfile.vercel:

Dockerfile.vercel
ENTRYPOINT ["sh", "-c", "export MEMOS_DSN=\"${MEMOS_DSN:-${DATABASE_URL_UNPOOLED:-$DATABASE_URL}}\"; for bin in /usr/local/memos/memos /usr/local/bin/memos /usr/bin/memos /memos; do if [ -x \"$bin\" ]; then exec \"$bin\"; fi; done; echo \"memos binary not found\" >&2; exit 127"]

This line does three jobs:

  • If MEMOS_DSN is already set, Memos uses it.
  • Otherwise, it maps Neon's DATABASE_URL_UNPOOLED to MEMOS_DSN, falling back to DATABASE_URL.
  • It starts the Memos binary from the upstream image.

The final Dockerfile.vercel looks like this:

Dockerfile.vercel
FROM neosmemo/memos:stable
# Vercel's container router is configured with PORT=5230 for this project.
# Memos runs as a non-root user, so it cannot bind to privileged port 80.
# Memos stores durable app data in Neon. The startup command maps Neon-provided
# database URLs to Memos' MEMOS_DSN when MEMOS_DSN is not set directly.
ENV MEMOS_ADDR=0.0.0.0 \
MEMOS_PORT=5230 \
MEMOS_DRIVER=postgres \
MEMOS_DATA=/tmp/memos
RUN mkdir -p /tmp/memos && chmod 1777 /tmp/memos
EXPOSE 5230
ENTRYPOINT ["sh", "-c", "export MEMOS_DSN=\"${MEMOS_DSN:-${DATABASE_URL_UNPOOLED:-$DATABASE_URL}}\"; for bin in /usr/local/memos/memos /usr/local/bin/memos /usr/bin/memos /memos; do if [ -x \"$bin\" ]; then exec \"$bin\"; fi; done; echo \"memos binary not found\" >&2; exit 127"]

Copy link to headingAdd a Services config (optional)

Vercel Services lets you deploy one or more services in a single Vercel project. If you later add a frontend, you can route only a path prefix to the Go container and send everything else to the frontend service:

vercel.json
{
"services": {
"web": {
"root": "apps/web",
"framework": "nextjs"
},
"api": {
"root": "apps/api",
"runtime": "container",
"entrypoint": "Dockerfile.vercel"
}
},
"rewrites": [
{ "source": "/api/(.*)", "destination": { "service": "api" } },
{ "source": "/(.*)", "destination": { "service": "web" } }
]
}

Copy link to headingDeploy to Vercel

From the project root:

Terminal
vercel deploy --prod

During deployment, Vercel builds the image, pushes it to Vercel Container Registry, creates a Function for the container service, and aliases the production URL.

After deploy, verify the app:

Terminal
curl -I https://your-project.vercel.app
vercel inspect https://your-project.vercel.app
vercel logs https://your-project.vercel.app --since 10m --expand

Copy link to headingTroubleshooting

Copy link to headingThe deployment is ready, but the site returns 404

Cause: The container built, but no public route points to it. In Services mode, a service is private until a top-level rewrite exposes it.

Fix: Add rewrites in the vercel.json:

vercel.json
{
"rewrites": [
{
"source": "/(.*)",
"destination": { "service": "memos" }
}
]
}

Also, confirm the project framework is set to Services when using services in vercel.json.

Copy link to headingThe container returns 500

Cause: The app is trying to listen on port 80 while running as a non-root user. Memos runs as a non-root user, so binding to privileged ports can fail.

Fix: Set Memos to listen on an unprivileged port:

Dockerfile
ENV MEMOS_PORT=5230
EXPOSE 5230

Then set the Vercel project environment variable:

.env
PORT=5230

Vercel uses PORT to decide where to route traffic inside the container. Memos uses MEMOS_PORT to decide where the app listens. You need both.

Copy link to headingBuild logs say no functions or static directory

Cause: In the Services and container path, this warning can appear even when Vercel produced the container-backed Function.

Fix: Inspect the deployment instead of relying only on that warning. The app is deployed as a container image.

Terminal
vercel inspect <deployment-url>

Copy link to headingFAQ

How long can a Dockerized Go app run?

Container image services inherit Vercel Function limits. They are designed for HTTP request handling, not permanent background daemons.

Can I use SQLite inside the container?

Not for durable production data. Vercel containers are stateless and can scale down or be replaced. Use a Postgres database integration, such as Neon, for persistent data.

Can I deploy a frontend and Dockerized Go backend together?

Yes. Use Vercel Services with one frontend service and one container service, then route /api/* or another path to the backend.

Where should I put secrets?

Use Vercel environment variables or integration-injected variables. Do not put database URLs, API keys, or credentials in the Dockerfile or repository.

Can I use WebSockets with a Dockerized Go app?

Vercel supports WebSockets for Vercel Functions on Fluid compute, including container-image Functions, with current platform limits. For long-lived connections, design clients to reconnect.

More Vercel Container Registry guides