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
| Field | What to enter |
|---|---|
| Published port | The port opened on the server, for example 25565. |
| Target port | The port the process listens on inside the container, for example 25565. |
| Protocol | TCP, UDP or TCP and UDP. |
| Bind to | Public 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.5Then 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:
| Port | Reserved for |
|---|---|
80, 443 | The edge proxy (HTTP and HTTPS for your domains). |
9145 | The edge's management API. |
20000 to 29999 | Synq'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:
| Answer | When |
|---|---|
409 PORT_UNAVAILABLE | A 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_PRIVILEGED | A database's port published below 1024, with a suggestedPort. |
400 INVALID_PORTS | The request itself is malformed: a port outside 1 to 65535, two rows on one port, a routed port bound to Localhost. |
409 PORT_IN_USE | Docker 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:5432or203.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:
- 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. - For a server you added over SSH with a DNS name, that name.
- 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.ioaddresses are never used here; they only spell out the IP. - 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:
| Why | Answer |
|---|---|
| Another service or project on the server publishes it | 409, 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 it | 409, names the program and fills in a free port |
It is below 1024 | 400: 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.
Related guides
Internal networking
How your apps and services reach each other privately by name, how each project gets its own network, and how to connect a database from one project to an app in another.
Custom domains, DNS & SSL
Put your app on your own address like app.example.com, or on a free subdomain of your install, with automatic HTTPS. Add the domain, point one DNS record at your server, verify, done.