Connect from your laptop
Create a database or bucket in the UI with New resource, or with the CLI on the machine running evertap or a paired laptop:
evertap create postgres blogEither shows its connection details. Every database URL and bucket endpoint points at ev.internal
on the engine's standard port:
| Service | In the connection details |
|---|---|
| PostgreSQL | postgres://<user>:<password>@ev.internal:5432/<database> |
| MySQL | mysql://<user>:<password>@ev.internal:3306/<database> |
| Redis | redis://<user>:<password>@ev.internal:6379/0 |
| RustFS bucket | Endpoint http://ev.internal:9000, region us-east-1 |
| Garage bucket | Endpoint http://ev.internal:3900, region garage |
The address is the same in every mode, for every version, and on every machine, so a project's
.env does not change when you change how evertap is reached or work from another laptop.
evertap env <name> prints the lines to paste:
DATABASE_URL=postgres://blog:…@ev.internal:5432/blogAWS_ENDPOINT_URL_S3=http://ev.internal:9000
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=…
AWS_SECRET_ACCESS_KEY=…
S3_BUCKET=photos
S3_FORCE_PATH_STYLE=trueWhat makes ev.internal reach evertap depends on the mode:
- In
localmode, evertap itself listens atev.internalon the machine it runs on, and setup already pointed the name there. Apps on that machine connect with nothing more to do. - In
networkandcloudflaremode, each laptop runsevertap connect, which listens atev.internaland carries each connection to evertap. The rest of this page sets that up.
Pair the laptop
Install the binary on the laptop. It needs no Docker. Then pair it with evertap:
evertap login https://evertap.example.comGive the address you open evertap at, such as https://evertap.example.com or
http://192.168.1.20:8080. Without http:// or https://, an IP address, localhost, or a
.local name gets http://, and any other name gets https://.
login prints a code and opens the approval page, <address>/device?code=…, in your browser. In a
browser that is signed in to evertap, check that the code matches the one in your terminal, and
approve it. On a laptop without a browser, open <address>/device on any signed-in device and type
the code. The code expires in 10 minutes.
login saves the address and this laptop's token in ~/.config/evertap/config.json, readable by
your user alone. Over plain http:// to another machine, it warns you: anyone watching that network
can read the token and act as you. The laptop then appears under Clients in the UI, where you can
revoke it. evertap logout revokes the token and forgets the address.
Run evertap connect
evertap connectIt listens on 127.77.0.1 at 5432, 3306, 6379, 9000, and 3900, and carries every connection over its
own WebSocket to evertap's address. Keep it running while you work, in its own terminal; stop it with
Ctrl+C.
The first time, point ev.internal at 127.77.0.1. connect prints the command until the line is
there.
Linux
echo "127.77.0.1 ev.internal" | sudo tee -a /etc/hostsLinux answers on all of 127.0.0.0/8, so the address needs nothing else.
macOS
macOS has only 127.0.0.1 on loopback, so connect first asks you to add the address:
sudo ifconfig lo0 alias 127.77.0.1 netmask 255.255.255.255
echo "127.77.0.1 ev.internal" | sudo tee -a /etc/hostsThe alias lasts until the Mac restarts. Run the ifconfig line again after a restart, before
evertap connect.
Windows
There is no Windows build. Run the CLI and your apps inside WSL and follow the Linux steps. Windows
apps outside WSL do not reach ev.internal there, because WSL passes only 127.0.0.1 on to
Windows.
WSL writes a new /etc/hosts each time it starts, which drops the ev.internal line. To keep it,
add this to /etc/wsl.conf in WSL, and restart WSL with wsl --shutdown from Windows:
[network]
generateHosts = falseCheck it
evertap status # which evertap this CLI uses, and whether it is signed in
psql "$(evertap url blog)" # connects through ev.internalWhen a port is taken
If something on the laptop already holds one of the ports on every address, connect says which
port, what likely holds it, and the command that shows it, such as
sudo lsof -nP -iTCP:5432 -sTCP:LISTEN. The other ports keep working. Something that listens on
127.0.0.1 alone does not get in the way, because 127.77.0.1 is a different address.
--engine <service>carries only the services you name, such asevertap connect --engine postgresql --engine redis.--host <address>listens on another address. Pointev.internalat it instead, since the connection details still sayev.internal. An address other machines can reach lets anyone there use your connections with your sign-in, leaving only each database's password in the way, andconnectwarns about it.
Databases you still run with Compose
Publish them on 127.0.0.1, which leaves 127.77.0.1 free:
services:
db:
image: postgres:18
ports:
- "127.0.0.1:5432:5432"With "5432:5432", Compose listens on every address: while it runs, evertap connect cannot listen
on 5432, and while evertap connect runs, that Compose file fails to start. The same goes for
evertap itself in local mode.
Apps in containers
An app in a container does not reach ev.internal, just as it does not reach your laptop's
localhost. An app that runs in Docker keeps its database in its own Compose file.
Connection pools
Each new connection opens a WebSocket to evertap, over TLS when evertap's address is https://, so
opening one costs more than with a database on your laptop. Use a connection pool.
Connections can also drop: when evertap restarts, when your network changes, or when a proxy or
tunnel in between restarts. evertap and evertap connect ping each other every 25 seconds and close
both ends after two pings go unanswered, so your app sees the error instead of waiting. Have the pool
check a connection before it uses it, and replace connections after about 30 minutes:
| Library | Setting |
|---|---|
| node-postgres | new Pool({ maxLifetimeSeconds: 1800 }) |
| SQLAlchemy | create_engine(url, pool_pre_ping=True, pool_recycle=1800) |
Go database/sql | db.SetConnMaxLifetime(30 * time.Minute) |
| Django | CONN_MAX_AGE = 1800 and CONN_HEALTH_CHECKS = True |
| HikariCP | maxLifetime is 30 minutes by default |
| Prisma | Leave pgbouncer=true out of the URL; see below |
PostgreSQL connections go through PgBouncer in transaction mode, so the server connection behind
yours can change between transactions. Prepared statements that drivers make work, as evertap's
PgBouncer keeps track of them, so Prisma needs no pgbouncer=true. Session state does not carry
over: SET (use SET LOCAL inside a transaction), LISTEN, session advisory locks, and temporary
tables last only until the transaction ends.
Buckets
Use the endpoint with path-style addressing, since <bucket>.ev.internal does not resolve. No AWS
SDK reads S3_FORCE_PATH_STYLE, so pass it in code: forcePathStyle: true in the AWS SDK for
JavaScript, or Config(s3={"addressing_style": "path"}) in boto3.
Presigned URLs point at ev.internal too, so they open only where evertap connect runs, or on the
machine running evertap in local mode. A page on http://localhost can upload to them from the
browser, as every bucket allows any origin; a page served over HTTPS cannot load an http://
address.
Signed links that work anywhere
When a link has to open elsewhere, such as in a preview deployment's browser, on a phone, or in an outside service that fetches the file, turn on Reachable from anywhere on the bucket's page, or run:
evertap reachable photos # evertap reachable photos --off turns it offObjects still need a signature: this is not S3's public access, and a link without one is refused.
It takes network or cloudflare mode, and an address of buckets' own, such as
https://s3.example.com, that your proxy or
tunnel serves. A tunnel that setup creates serves
them at s3.<your domain> already. local mode does not offer it.
The bucket's endpoint stays at ev.internal, so the rest of your app works as before, and its
.env gains one line:
S3_PUBLIC_ENDPOINT=https://s3.example.comSign links with a second client on that address, and keep your app's client on
AWS_ENDPOINT_URL_S3. Signing happens in your app, so the second client makes no requests:
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
// Only for signing links; every other call keeps to AWS_ENDPOINT_URL_S3
const signer = new S3Client({
endpoint: process.env.S3_PUBLIC_ENDPOINT,
forcePathStyle: true,
// Otherwise the link carries the checksum of an empty body, and Garage refuses the upload
requestChecksumCalculation: "WHEN_REQUIRED",
});
const url = await getSignedUrl(
signer,
new PutObjectCommand({ Bucket: process.env.S3_BUCKET, Key: "avatar.png" }),
{ expiresIn: 3600 },
);import os
import boto3
from botocore.config import Config
# Only for signing links; every other call keeps to AWS_ENDPOINT_URL_S3
signer = boto3.client(
"s3",
endpoint_url=os.environ["S3_PUBLIC_ENDPOINT"],
# boto3 reads AWS_DEFAULT_REGION, not AWS_REGION
region_name=os.environ["AWS_REGION"],
config=Config(signature_version="s3v4", s3={"addressing_style": "path"}),
)
url = signer.generate_presigned_url(
"put_object",
Params={"Bucket": os.environ["S3_BUCKET"], "Key": "avatar.png"},
ExpiresIn=3600,
)- Links are path-style,
https://s3.example.com/photos/avatar.png: one address cannot hold each bucket's name in its hostname. - Form uploads (a presigned POST) work with Garage buckets. RustFS does not take them.
- Through a Cloudflare Tunnel, see its limits.
CI with an API key
A CI job cannot approve evertap login, so it uses an API key instead.
- In the UI, open Clients → New API key and name it. The key appears once, as
EVERTAP_API_KEY=evertap_…: evertap keeps only its hash. - Store it as a secret in your CI, and set
EVERTAP_URLto the address you open evertap at. With both set, the CLI needs nologin. - In the job, create what the tests need, and carry connections with
evertap connectas on a laptop:
curl -fsSL https://github.com/caru-ini/evertap/releases/latest/download/install.sh | sh
name="ci_${GITHUB_RUN_ID}"
evertap create postgres "$name" --temporary
export DATABASE_URL="$(evertap url "$name")"
echo "127.77.0.1 ev.internal" | sudo tee -a /etc/hosts
evertap connect --engine postgresql &
timeout 10 bash -c 'until echo > /dev/tcp/127.77.0.1/5432; do sleep 0.5; done'
# run the tests
evertap delete "$name" --yes--temporary deletes the database after 7 days without a connection, in case the last step never
runs. The CI runner has to reach evertap's address, so evertap on a home network needs a proxy or a
tunnel with a public address, or a runner on that network.
An API key can do everything a paired CLI can with resources, including reading their data and deleting those that are not protected. It cannot approve sign-ins, create API keys, or revoke anything; those stay in the UI. Revoke a key under Clients when it leaks or you stop using it.
The evertap name and logo are not licensed with the code (section 6 of the license). You may use them to refer to evertap, but not to name or brand your own product or service, or in a way that suggests evertap made or endorses it, without permission.
evertap is an independent project. It is not affiliated with, endorsed, sponsored, supported, or certified by the owners of the software it runs, and it uses their names only to say which software that is.
- Postgres, PostgreSQL and the Slonik Logo are trademarks or registered trademarks of the PostgreSQL Community Association of Canada, and used with their permission.
- MySQL is a registered trademark of Oracle and/or its affiliates.
- Redis is a registered trademark of Redis Ltd. Any rights therein are reserved to Redis Ltd.
- RustFS is a trademark of RustFS, Inc.
- Other names, including Garage, may be trademarks of their respective owners.