Guides

Public ports & database access

Publish TCP or UDP ports on your server for traffic a domain can't carry, keep a port on localhost for SSH tunnels, and expose a database to the internet with one switch.

Websites and APIs reach your app through a domain: the edge proxy on ports 80 and 443 takes the request and hands it to the container. Some traffic can't go that way: a database client, an SMTP or IMAP server, a game server, a UDP service, a raw TCP protocol. For those you publish a port: the server opens it and forwards it straight into the container.

Nothing is published by default. Inside a project, services already reach each other on their private network (see Internal networking), so you only publish what something outside the project must reach.

What you need first

  • A project that runs a container on one of your own servers. A static site has no container, so it has no ports to publish.
  • Permission to change the project's settings.

Where the Ports editor is

  • A single app: open the project and choose Ports in the project menu.
  • A compose or monorepo project: ports are set per service. The project's Ports entry says so and links to Apps & Services; open the service and go to its Settings tab. The Ports card is at the top.

Publish a port

Open the editor

Press Add port (or Edit ports when some are already published).

Fill in the row

FieldWhat to enter
Published portThe port opened on the server, for example 25565.
Target portThe port the process listens on inside the container, for example 25565.
ProtocolTCP, UDP or TCP and UDP.
Bind toPublic listens on every network interface of the server. Localhost listens on 127.0.0.1 only.

Press Add port for another row, or the trash icon to remove one. Up to 50 ports per service or app.

Save

Press Save ports. If the app or service is running, Synq recreates its container with the new bindings right away, keeping its image, environment, volumes, internal name and domains. The message says Ports saved and applied to the running container. If nothing is deployed yet, it says Ports saved. They apply on the next deploy.

Each row bound to Public then shows Reachable at with the server address and port, for example Reachable at 203.0.113.5:25565 (when Synq knows the server's address). The address is the server's connection hostname or the panel's domain when one points at the server (see Which host the address uses). A Localhost row shows no address.

If the save worked but the container swap failed (for example a deployment was running), the card keeps the new list and shows Apply now. Press it once the deployment has finished.

Published ports turn off zero-downtime deploys

A fixed port on the server can't be bound by two containers at once. So once a single app publishes any port, each deploy stops the old container before starting the new one, even with Zero downtime on (see Container settings). Expect a short gap during deploys; the deploy log says why.

Public or Localhost

  • Public is for clients on the internet or another server: a database you manage from your laptop, a mail server, a game server.
  • Localhost is for tools on the server itself and for SSH tunnels. Nothing outside the server can connect, but you can still reach it from your computer through SSH:
# Forward local port 5432 to a Localhost-bound port 5432 on the server
ssh -N -L 5432:127.0.0.1:5432 root@203.0.113.5

Then point your client at localhost:5432.

Ports you can't use

Synq refuses a port it needs for itself. The editor marks the row, for example Port 443 is reserved by Synq on this server. A request made directly to the API gets a message that names what the port is reserved for. The reserved ports are:

PortReserved for
80, 443The edge proxy (HTTP and HTTPS for your domains).
9145The edge's management API.
20000 to 29999Synq's internal routing. The edge reaches each app on a private loopback port in this range.
4000 and 3001 (on the server that runs Synq)The Synq API and panel. If you changed their ports, the reserved ports follow.

It also refuses a port that is already taken on the same server: by another row in the list, by another service or project (of any team) that publishes it, or by a program that is already listening on it. The error names who holds it, nothing is saved, and the row offers Use port N with a free port next to it (or Auto-assign, which leaves the choice to Synq). Two different servers can publish the same port.

A database's own port (5432 for PostgreSQL, for example) can't be published below 1024: those ports are kept for system services. Other services can use them, so a mail server can still publish 25 and a DNS server 53.

Every change to published ports on a server (this editor, Public access for a database, a service update over the API, a compose import or sync, an app install) takes that server's lock from the moment it checks which ports are free until it has saved (and applied) its own, so two changes made at the same moment never end up with the same port. When applying the new ports to the running container fails, the saved list goes back to what it was and the message says why.

Over the API, the answers are:

AnswerWhen
409 PORT_UNAVAILABLEA well-formed port something else holds (another service or project, a port Synq reserves, a program on the server). issues[] names each row's holder with a suggestedPort, and the body repeats the first as suggestedPort.
400 PORT_PRIVILEGEDA database's port published below 1024, with a suggestedPort.
400 INVALID_PORTSThe request itself is malformed: a port outside 1 to 65535, two rows on one port, a routed port bound to Localhost.
409 PORT_IN_USEDocker couldn't bind the port when the container was recreated. Nothing was changed.

One more rule: an app's routed port (the one its domain uses) can't be published on Localhost over TCP, because it is already reachable on the server through the domain. Publish it Public or target another port.

An app that runs replicas can't publish ports at all: a host port can be held by one container only. Set Replicas back to 1 first; the edge reaches every replica without a published port.

Public ports skip the host firewall

Docker publishes ports with its own firewall rules, so ufw and similar host firewalls do not block them. Publish only what must be reachable, protect the service with a strong password, and restrict source IPs at your cloud provider's firewall (security group) where you can.

Public access for a database

A database service (PostgreSQL, MySQL, MariaDB, MongoDB, Redis or Valkey) is private to its project by default. To connect to it from your computer or from another server, turn on Public access instead of typing port rows.

Open the database service

Open the project, go to Apps & Services and click the database. The Public access card is on its Overview tab. (It only appears when Synq recognises the image as one of the databases above.)

Turn on Expose to the internet

Switch on Expose to the internet. Leave Public port empty to use the engine's standard port (5432, 3306, 27017 or 6379), or the next free port when another app on the server already uses it. Type a port, for example 15432, to pick one yourself.

Confirm

Read the warning and press Expose database. If the service is running, its container is updated right away and the toast says the database is now reachable from the internet. If the service isn't deployed yet, the toast says Saved. It applies on the next deploy.

The card then shows two addresses, each with a copy button:

  • Internal, from other services in this project, for example db:5432.
  • External, from anywhere on the internet, for example db.example.com:5432 or 203.0.113.5:5432.

When Synq can read the database's credentials, it also shows full connection URLs with the password hidden. Copy one and put in the real password yourself:

psql "postgresql://app:your-password@db.example.com:5432/app"

The same card is on the project's connection details as External access, with a ready-to-run command line and an SSH tunnel command.

To close it again, switch Expose to the internet off. The public port is removed and the database is private again. Any other port rows the service has are left alone. This also works when the container is already gone: the port is removed and nothing is published on the next deploy.

Which host the address uses

Under the address, the card says which host it uses and why, for example Using app.example.com (points at this server). Synq picks it in this order:

  1. The server's connection hostname, when you set one (server page, Security tab, Connection hostname, or synq server update <server-id> --public-hostname db.example.com). Synq uses it exactly as typed and does not check where it points.
  2. For a server you added over SSH with a DNS name, that name.
  3. For the server Synq runs on: the panel's own hostname, then your apps domain, each only when its public DNS points straight at this server's IP address. A name behind a proxy (for example Cloudflare's orange cloud) or pointed at another machine is skipped, and the card says so, because a database client can't connect through an HTTP proxy. sslip.io addresses are never used here; they only spell out the IP.
  4. Otherwise, the server's IP address.

Synq caches the DNS answers for five minutes. If a lookup fails, the card uses the IP address rather than failing.

A hostname is not a secret

Anyone can look up the hostname and find the server's address, just as they can scan the IP. It makes the URL easier to read, not private. For private access, use a Localhost port and an SSH tunnel.

Several databases on one server

Each database needs its own public port, since two containers can't listen on the same port of one server. With the port left empty, the first PostgreSQL gets 5432, the next 5433, then 5434, even when you turn them on at the same moment. The card shows the port that was actually assigned in its URL and command line, and says why it isn't the standard one, for example Port 5432 is used by service "db" in "Shop", so PostgreSQL uses port 5433.

A port you type is never moved. If it can't be used, nothing changes and the card offers a free port instead:

WhyAnswer
Another service or project on the server publishes it409, names the holder and fills in a free port
Synq reserves it (the edge, the panel, internal routing)409, says what it is reserved for and fills in a free port
A program outside Synq listens on it409, names the program and fills in a free port
It is below 1024400: ports below 1024 are kept for system services

If updating the running container fails (Docker can't bind the port, a deployment is in progress, or the server can't be reached), Synq puts the container and the saved ports back as they were and says why. If Docker starts the container but doesn't publish the port, the card says so instead of showing an address that won't connect; redeploy the service and check again.

Your cloud firewall must allow the port

Synq can't see your cloud provider's firewall from inside the server. On AWS, for example, add an inbound rule for the port to the instance's security group, ideally only from your own IP address.

Anyone can try to connect

An exposed database gets scanned and brute-forced within minutes. Use a long random password, allow only your own IP addresses at your provider's firewall where you can, and turn public access off when you no longer need it. For occasional access, a Localhost port plus an SSH tunnel is safer.

Compose files and ports:

A service imported from a docker-compose.yml keeps the ports: entries it declares. A host:container entry with no interface is published on localhost (127.0.0.1), not on every interface as plain Docker would do; write 0.0.0.0:5432:5432 in the file, or use the editor, to make it public. Entries the editor can't represent (a container port on its own, a specific IP address, a port range, sctp) are listed under Kept as written and left untouched; change those in the service settings or the file.

From the API

# List a single app's published ports, reserved ports and server address
curl -H "Authorization: Bearer $SYNQ_TOKEN" \
  "$SYNQ_URL/api/projects/$PROJECT_ID/ports"

# Replace them: publish 25565 over TCP on every interface, and apply now
curl -X PUT -H "Authorization: Bearer $SYNQ_TOKEN" -H "Content-Type: application/json" \
  -d '{"ports":[{"hostPort":25565,"containerPort":25565,"protocol":"tcp","bind":"public"}],"apply":true}' \
  "$SYNQ_URL/api/projects/$PROJECT_ID/ports"

# Expose a database service on port 15432
curl -X PUT -H "Authorization: Bearer $SYNQ_TOKEN" -H "Content-Type: application/json" \
  -d '{"enabled":true,"hostPort":15432}' \
  "$SYNQ_URL/api/projects/$PROJECT_ID/services/$SERVICE_ID/public-access"

SYNQ_URL is your panel address (for example https://app.example.com). Services use /api/projects/:id/services/:serviceId/ports with the same body, and both have a /ports/apply endpoint that re-applies the saved list to the running container.

The public-access answer carries serverHost (what clients dial) and connectionHost, which says how it was chosen: source is server-hostname, ssh-host, panel-domain, apps-domain or ip, and skipped lists names that were tried and not used (mismatch when they point elsewhere, unresolved when DNS didn't answer). defaultPortHeldBy names who holds the standard port when the database uses another one. After a save, portReassignments lists ports that moved, and unboundPorts lists ports Docker didn't publish. A refused port answers 409 (PORT_UNAVAILABLE) or 400 (PORT_PRIVILEGED) with a suggestedPort you can send instead.

Troubleshooting

Port 443 is reserved

Web traffic on 80 and 443 belongs to the edge. Serve the app through a domain instead of publishing those ports.

The port is already in use

Another project, another service, or a program on the server holds it. The message names the holder and nothing is saved. Press Use port N to take the free port Synq found, free the port where it is used, or choose a different published port; the target port inside the container can stay the same.

I published a port but can't connect from outside

Check that the row is Public, not Localhost, and that your cloud provider's firewall or security group allows the port. Docker bypasses ufw on the server itself, but not your provider's network firewall.

On this page