# Rocketeers > Self hosting made effortless for developers and agencies. Marketing site and knowledge base: https://rocketeersapp.com Application dashboard: https://rocketeersapp.com ## Site structure - **Home**: https://rocketeersapp.com/ — product overview - **Features**: https://rocketeersapp.com/features — core product features and capabilities - **Providers**: https://rocketeersapp.com/providers — supported cloud, DNS, email and notification providers - **Pricing**: https://rocketeersapp.com/pricing - **Knowledge base**: https://rocketeersapp.com/knowledge — 170 technical articles on self-hosting, server management, DNS, email deliverability, SSL and web performance - **Documentation**: https://rocketeersapp.com/docs — app guides and API reference - **Tools**: https://rocketeersapp.com/tools — free DNS, email, SSL and performance checkers ## AI ### Use the OpenAI API in Laravel Source: https://rocketeersapp.com/use-the-openai-api-in-laravel Adding AI to a Laravel app comes down to a few lines: install the client, store your key, and call the chat endpoint. Here is how to set it up cleanly, stream responses, and keep your key and your bill under control. Wiring an LLM into a Laravel app is less work than most people expect. The [openai-php/laravel](https://github.com/openai-php/laravel) package gives you a typed client and a facade, so calling the chat endpoint is a single method call. Here's a clean setup, from installing the client to streaming responses and keeping your key safe. ## Install the client Pull in the package with Composer: ```bash composer require openai-php/laravel ``` Then publish its config file: ```bash php artisan vendor:publish --provider="OpenAI\Laravel\ServiceProvider" ``` That creates `config/openai.php`, which reads your credentials from the environment. ## Store your API key Put your key in `.env` — never in code or version control: ```ini OPENAI_API_KEY=sk-your-key-here ``` The published config already references it, so there's nothing else to wire up: ```php // config/openai.php 'api_key' => env('OPENAI_API_KEY'), ``` On a production server, set `OPENAI_API_KEY` as a real environment variable or a secret rather than shipping the `.env`. If the key ever leaks, rotate it in your OpenAI dashboard immediately. ## Make your first call With the facade, a chat completion is one call: ```php use OpenAI\Laravel\Facades\OpenAI; $response = OpenAI::chat()->create([ 'model' => 'gpt-4o-mini', 'messages' => [ ['role' => 'system', 'content' => 'You are a concise assistant.'], ['role' => 'user', 'content' => 'Summarise what a reverse proxy does.'], ], 'max_tokens' => 200, ]); return $response->choices[0]->message->content; ``` Picking a small model like `gpt-4o-mini` and capping `max_tokens` are the two cheapest habits for keeping your bill predictable. ## Stream the response For a chat UI you want text to appear as it's generated rather than after a long pause. Use the streaming variant and emit each chunk: ```php use OpenAI\Laravel\Facades\OpenAI; return response()->stream(function () { $stream = OpenAI::chat()->createStreamed([ 'model' => 'gpt-4o-mini', 'messages' => [['role' => 'user', 'content' => 'Write a haiku about servers.']], ]); foreach ($stream as $chunk) { echo $chunk->choices[0]->delta->content ?? ''; ob_flush(); flush(); } }, 200, [ 'Content-Type' => 'text/event-stream', 'X-Accel-Buffering' => 'no', ]); ``` The `X-Accel-Buffering: no` header matters behind [Nginx](/how-to-install-nginx), which otherwise buffers the whole response and defeats the point of streaming. ## Handle errors and limits Two failures are worth handling explicitly. The first is the rate limit — under load OpenAI returns HTTP 429, and you should back off and retry rather than hammer the endpoint; the dedicated guide is [fix the OpenAI API rate limit (429) error](/fix-openai-api-rate-limit-429-error). The second is cost: set a hard spending limit in your account, cache repeated prompts, and reach for the smallest model that does the job. ## Or run it locally If you'd rather not depend on a paid API at all, the same code can point at a local model. [Ollama](/run-a-local-llm-with-ollama) exposes an OpenAI-compatible endpoint, so changing the base URL to `http://localhost:11434/v1` lets this exact Laravel setup talk to a model running on [your own server](/self-host-an-llm-on-your-own-server). ### Track AI bots visiting your website Source: https://rocketeersapp.com/track-ai-bots-visiting-your-website AI crawlers like GPTBot, ClaudeBot and PerplexityBot scrape sites to train models and answer questions. Here is how to spot them in your logs, log them to their own file, and decide what to allow. AI crawlers now make up a real slice of traffic. Some scrape pages in bulk to train models, others fetch a single page in real time because someone asked an assistant about it. Either way, you can see exactly who's visiting — they announce themselves in the `User-Agent` header. Here's how to track them. ## Know the user agents The major AI companies publish and respect their crawler names. The ones worth watching for: | User agent | Operator | Purpose | | --- | --- | --- | | `GPTBot` | OpenAI | Training crawl | | `ChatGPT-User` | OpenAI | Live fetch for a user prompt | | `ClaudeBot`, `anthropic-ai` | Anthropic | Training crawl | | `Google-Extended` | Google | AI training (Gemini) | | `PerplexityBot` | Perplexity | Search index | | `CCBot` | Common Crawl | Open dataset many models train on | | `Bytespider` | ByteDance | Training crawl | | `Amazonbot`, `Applebot-Extended`, `Meta-ExternalAgent` | Amazon, Apple, Meta | AI training | They all match cleanly by name, which is what makes tracking them straightforward. ## Find them in your access log The quickest check needs nothing but the log you already have. Grep the access log for the names: ```bash grep -iE "GPTBot|ClaudeBot|Google-Extended|PerplexityBot|CCBot|Bytespider|anthropic-ai" \ /var/log/nginx/access.log ``` To see which AI bots hit you most over the current log, pull out the user agent and count: ```bash grep -ioE "GPTBot|ClaudeBot|Google-Extended|PerplexityBot|CCBot|Bytespider|ChatGPT-User" \ /var/log/nginx/access.log | sort | uniq -c | sort -rn ``` That one line tells you whether you're being crawled for training (lots of `GPTBot`/`ClaudeBot` hits across many URLs) or fetched live (occasional `ChatGPT-User`). ## Log AI bots to their own file Grepping is fine for a spot check, but for ongoing tracking it's cleaner to route AI-bot requests into a dedicated log. Use an [Nginx](/how-to-install-nginx) `map` to flag the user agent, then log conditionally: ```nginx map $http_user_agent $ai_bot { default ""; "~*GPTBot" "openai"; "~*ChatGPT-User" "openai"; "~*ClaudeBot" "anthropic"; "~*anthropic-ai" "anthropic"; "~*Google-Extended" "google"; "~*PerplexityBot" "perplexity"; "~*CCBot" "commoncrawl"; "~*Bytespider" "bytedance"; } server { # Write AI-bot hits to their own file, and only those. access_log /var/log/nginx/ai-bots.log combined if=$ai_bot; } ``` Reload Nginx and the new file fills up with AI-bot traffic only: ```bash nginx -t && systemctl reload nginx tail -f /var/log/nginx/ai-bots.log ``` This is the same conditional-logging technique covered in [log bot requests in nginx](/log-bot-requests-nginx) and [detect Googlebot in nginx](/detect-googlebot-nginx), narrowed to the AI crawlers. ## Control what they can do Once you can see them, you can decide. Well-behaved crawlers honour `robots.txt`, so you allow or disallow each by name: ``` User-agent: GPTBot Disallow: / User-agent: Google-Extended Disallow: / User-agent: PerplexityBot Allow: / ``` For agents that ignore `robots.txt`, enforce it at the web server and return a 403 using the `$ai_bot` variable you already defined: ```nginx if ($ai_bot) { return 403; } ``` Bear in mind the trade-off: blocking training crawlers can also drop your site out of the answers those assistants give. Many sites choose to *allow* AI bots precisely so they get cited, and just track the volume. If you want to point crawlers at a curated, machine-readable summary of your site instead, an `llms.txt` file is the emerging convention for that. ## Check any site from the outside To see how a site treats AI crawlers without digging through logs — which agents it allows, what its `robots.txt` and `llms.txt` say — run it through the [AI scan tool](/ai-scan). It fetches the same signals an AI bot would and reports how the site responds, which is a fast way to audit your own setup or a competitor's. ### Run DeepSeek locally Source: https://rocketeersapp.com/run-deepseek-locally DeepSeek's open-weight reasoning models run on your own hardware, no API key required. Here is how to pull and run DeepSeek-R1 with Ollama, pick a size that fits your machine, and query it over HTTP. DeepSeek releases its models as open weights, which means you can download DeepSeek-R1 and run it entirely on your own hardware. No account, no API key, no per-token bill, and your prompts never leave the machine. The easiest way to do it is with [Ollama](/run-a-local-llm-with-ollama), which handles the download and serves the model over HTTP. ## Install Ollama If you don't already have it, install Ollama first: ```bash curl -fsSL https://ollama.com/install.sh | sh ``` On macOS, `brew install ollama` or the desktop app work too. See [run a local LLM with Ollama](/run-a-local-llm-with-ollama) for the full setup. ## Pull and run DeepSeek-R1 Run the model and Ollama downloads the weights on first use: ```bash ollama run deepseek-r1 ``` That default tag is a distilled model that fits most machines. DeepSeek-R1 is a **reasoning model**, so it thinks out loud inside `` tags before giving its final answer. Ask it something with a few steps and you'll see the working: ``` >>> If a server handles 200 requests per second and each takes 40ms, how many run concurrently? ``` ## Pick a size that fits DeepSeek ships in several distilled sizes. Choose the largest that fits in memory, since a model has to fit in RAM or VRAM to run well: ```bash ollama run deepseek-r1:1.5b # laptop / CPU friendly ollama run deepseek-r1:7b # modest GPU ollama run deepseek-r1:14b # mid-range GPU ollama run deepseek-r1:32b # high-end GPU ollama run deepseek-r1:70b # 40 GB+ VRAM ``` The small distilled versions are fine-tuned to imitate the full R1's reasoning, so you keep much of the behaviour at a fraction of the size. Check what you've downloaded and remove sizes you're done with: ```bash ollama list ollama rm deepseek-r1:1.5b ``` ## Query it over HTTP Once it's pulled, the model is available on Ollama's API on port `11434`, so any script or app can call it: ```bash curl http://localhost:11434/api/generate -d '{ "model": "deepseek-r1", "prompt": "List three causes of a slow database query.", "stream": false }' ``` The response includes the reasoning trace as well as the answer. If you only want the final answer, strip everything up to and including the closing `` tag before you display it. ## Run it for a team For shared use, put DeepSeek on a server with a GPU rather than a laptop. The steps are the same as any model: bind Ollama to all interfaces, then protect it with a reverse proxy and a token. The full hardening walkthrough — sizing the box, [Nginx](/how-to-install-nginx) in front, TLS, firewalling the port — is in [self-host an LLM on your own server](/self-host-an-llm-on-your-own-server). ### Run a local LLM with Ollama Source: https://rocketeersapp.com/run-a-local-llm-with-ollama Ollama is the quickest way to download and run an open large language model on your own machine, no API keys and no per-token bill. Here is how to install it, pull a model, and talk to it from the command line and over HTTP. [Ollama](https://ollama.com) packages an open large language model, its weights, and a small HTTP server into a single command. You download a model once and run it entirely on your own hardware, which means no API keys, no rate limits, and no data leaving your machine. It's the fastest path from "I want to try an LLM" to a working prompt. ## Install Ollama On Linux the one-line installer sets up the binary and a systemd service: ```bash curl -fsSL https://ollama.com/install.sh | sh ``` On macOS download the app from the website, or use Homebrew: ```bash brew install ollama ``` The installer registers Ollama as a background service. Confirm it's running: ```bash ollama --version systemctl status ollama # Linux ``` ## Pull and run your first model Pick a model and run it. Ollama downloads the weights on first use, then drops you into an interactive prompt: ```bash ollama run llama3.2 ``` The first run pulls a few gigabytes, so give it a moment. After that the model is cached and starts instantly. Type a question, get an answer, and press `Ctrl+D` to exit. If your machine is modest, start small. A 1B model is fast even on a laptop: ```bash ollama run llama3.2:1b ``` To see what you've downloaded and reclaim space: ```bash ollama list ollama rm llama3.2:1b ``` ## Talk to it over HTTP The reason Ollama is useful in real applications is the built-in REST API on port `11434`. Anything that speaks HTTP can use it: ```bash curl http://localhost:11434/api/generate -d '{ "model": "llama3.2", "prompt": "Explain a reverse proxy in one sentence.", "stream": false }' ``` It also exposes an OpenAI-compatible endpoint at `/v1/chat/completions`, so most libraries written for OpenAI work against Ollama by just changing the base URL. That makes it a drop-in local stand-in while you develop, before you decide whether to [use a hosted API like OpenAI](/use-the-openai-api-in-laravel) in production. ## Run it on a server Running Ollama on your laptop is fine for experiments, but for a shared model you'll want it on a server with a GPU. The principle is the same, with two changes: bind the API to all interfaces and put it behind a reverse proxy. ```bash sudo systemctl edit ollama ``` Add the host override: ```ini [Service] Environment="OLLAMA_HOST=0.0.0.0" ``` Then reload and restart: ```bash sudo systemctl daemon-reload sudo systemctl restart ollama ``` Do **not** open port `11434` to the internet directly, it has no authentication. Front it with [Nginx](/how-to-install-nginx) and require a token, the same way you'd protect any internal service. For a full walkthrough of sizing a box and locking it down, see [self-host an LLM on your own server](/self-host-an-llm-on-your-own-server). ## Try other models Ollama's library covers most popular open-weight models. A few worth knowing: - `llama3.2` — a solid general-purpose default from Meta. - `deepseek-r1` — a reasoning model, see [run DeepSeek locally](/run-deepseek-locally). - `qwen2.5-coder` — tuned for code generation. - `nomic-embed-text` — an embedding model for building search and retrieval. Swap the name in `ollama run` or `ollama pull` to try any of them. Because everything runs locally, the only limit is your hardware. ### Fix the OpenAI API rate limit (429) error Source: https://rocketeersapp.com/fix-openai-api-rate-limit-429-error A 429 from the OpenAI API means you have sent requests faster than your account is allowed, or run out of quota. Here is what the error actually means and how to fix it with backoff, batching, and the right limits. ## About the 429 error `429 Too Many Requests` is the OpenAI API telling you it's refusing a request because you've gone over a limit. It's the AI-API equivalent of any other HTTP rate-limit response — the request was well-formed, the server just won't process it right now. There are two very different causes hiding behind the same status code, and fixing the wrong one wastes time. ## Why do I see this error Read the JSON body first — it names the real cause: - **Rate limit exceeded** — you sent requests faster than your account's *requests per minute* (RPM) or *tokens per minute* (TPM) allows. This is a speed problem. - **`insufficient_quota`** — your account is out of credit or has hit its spending limit. This is a billing problem, and slowing down won't help. A 429 that appears even though you're barely sending any requests is almost always the second kind. ## Fix a speed-related 429 The correct response to a rate limit is to **back off and retry**, not to hammer the endpoint. Use exponential backoff with jitter: wait a moment, and double the wait on each retry, with a little randomness so multiple clients don't all retry at the same instant. ```python import time, random from openai import OpenAI, RateLimitError client = OpenAI() def chat_with_retry(messages, retries=5): for attempt in range(retries): try: return client.chat.completions.create( model="gpt-4o-mini", messages=messages ) except RateLimitError: if attempt == retries - 1: raise wait = (2 ** attempt) + random.random() time.sleep(wait) ``` The official SDKs already retry with backoff internally, so if you're using one, raise its retry count rather than writing your own loop. If you call the API directly over HTTP, honour the `Retry-After` header when it's present — it tells you exactly how long to wait. The same pattern applies in PHP; see [use the OpenAI API in Laravel](/use-the-openai-api-in-laravel). ## Reduce how much you send Backoff treats the symptom. To stop hitting the limit at all, lower your request and token rate: - **Batch** multiple items into one request instead of one call each. - **Cache** repeated or identical prompts so you don't pay to ask twice. - **Cap `max_tokens`** so a runaway response can't burn your TPM budget. - **Pick a smaller model** — it has higher rate limits and costs less per token. ## Fix a quota-related 429 If the body says `insufficient_quota`, no amount of backoff will help. Open your [OpenAI dashboard](https://platform.openai.com/account/billing) and: 1. Check your remaining credit and add a payment method if needed. 2. Review your monthly spending limit — you may have hit a cap you set yourself. 3. Confirm you're using the right API key for the right project. ## Raise your rate limit Rate limits scale with your **usage tier**, which increases automatically as your account spends more over time. You can't set the numbers by hand on the lower tiers, so the only durable fix is to let the tier mature while keeping usage efficient with the batching and caching above. If a fixed, predictable limit matters more to you than scaling, running a model on [your own server](/self-host-an-llm-on-your-own-server) removes the per-minute cap entirely. ### Self-host an LLM on your own server Source: https://rocketeersapp.com/self-host-an-llm-on-your-own-server Running a large language model on your own server keeps your data private and turns an unpredictable per-token bill into a fixed monthly cost. Here is how to size the machine, install a model, serve it over HTTP, and lock it down for production. Sending every prompt to a hosted API is the easy path, but it has two costs: your data leaves your network, and the per-token bill grows with usage. Self-hosting flips both. The model runs on hardware you control, nothing leaves your perimeter, and your spend is a fixed monthly server cost instead of a meter that never stops. Here's how to do it properly. ## Size the machine The single most important number is **VRAM** (or system RAM if you run on CPU). A model has to fit in memory to run well, and a rough rule is that a quantised model needs about its file size: - 1B–3B models: a few gigabytes, runs on almost anything. - 7B–8B models: ~5–6 GB, a single modest GPU. - 13B models: ~9 GB. - 70B models: 40 GB+, which means a high-end data-centre GPU or several cards. Add a couple of gigabytes of headroom for the context window and concurrent requests. If you're renting, a GPU instance from a provider like [Hetzner](/providers/hetzner) or a cloud GPU host is the usual starting point. CPU-only works for small models and light traffic, but expect it to be slow. ## Install a runtime The quickest, most reliable runtime is [Ollama](https://ollama.com), which handles model downloads, quantisation, and an HTTP server in one tool. Install it and pull a model: ```bash curl -fsSL https://ollama.com/install.sh | sh ollama pull llama3.2 ``` That's the whole setup. For the full walkthrough of running and querying models, see [run a local LLM with Ollama](/run-a-local-llm-with-ollama). If you need maximum throughput and are comfortable with more configuration, [vLLM](https://docs.vllm.ai) is the production-grade alternative that batches requests across a GPU. ## Serve it on all interfaces By default Ollama only listens on `127.0.0.1`. To let other machines on your network reach it, override the host in the systemd unit: ```bash sudo systemctl edit ollama ``` ```ini [Service] Environment="OLLAMA_HOST=0.0.0.0" ``` Reload and restart: ```bash sudo systemctl daemon-reload sudo systemctl restart ollama ``` The API now answers on port `11434` — but do not stop here, because that port has no authentication of its own. ## Put it behind a reverse proxy Never expose the raw inference port to the internet. Front it with [Nginx](/how-to-install-nginx), terminate TLS, and require a token. A minimal proxy that checks a shared secret looks like this: ```nginx server { listen 443 ssl; server_name ai.example.com; # ssl_certificate ... (see your certbot setup) location / { if ($http_authorization != "Bearer YOUR_SECRET_TOKEN") { return 401; } proxy_pass http://127.0.0.1:11434; proxy_set_header Host $host; proxy_read_timeout 300s; } } ``` Grab a free certificate with [Certbot](/how-to-install-certbot), then lock the inference port to localhost only with your firewall so nothing can bypass the proxy: ```bash sudo ufw deny 11434 ``` The long `proxy_read_timeout` matters: model responses can take many seconds, and a short timeout will cut them off, which often shows up as a [504 Gateway Timeout](/504-gateway-timeout-nginx). ## Add retrieval with a vector database A bare model only knows what it was trained on. To answer questions about your own documents, you store embeddings in a vector database and feed the relevant chunks into each prompt — the pattern known as RAG. PostgreSQL with the `pgvector` extension is a low-overhead way to do this on the same kind of server; see [install PostgreSQL with pgvector on Ubuntu](/install-postgresql-pgvector-ubuntu). ## Decide if it's worth it Self-hosting wins when your token volume is **high and steady** — a fixed server cost beats a meter that scales with traffic. It loses when usage is spiky or low, where a hosted API's pay-per-use model is cheaper and needs zero maintenance. Run the numbers on your real usage, and if you do go hosted, learn how to [use the OpenAI API in Laravel](/use-the-openai-api-in-laravel) and how to [handle its rate limits](/fix-openai-api-rate-limit-429-error). ## Command Line ### How to find your IP address from the Linux command line Source: https://rocketeersapp.com/find-ip-address-linux-command-line Find your local private IP with ip addr or hostname -I, and your public IP with curl, plus a note on the older ifconfig command. ## Two different IP addresses There's a catch that trips people up: your machine has two kinds of IP address. The local (private) one identifies you on your own network, like `192.168.1.42`. The public one is what the rest of the internet sees, assigned by your router or hosting provider. You find them in completely different ways. ## Finding your local (private) IP The modern tool is `ip`. To list every interface and its addresses: ```bash ip addr ``` That's a lot of output. `ip a` is the same thing, just shorter. Look for your active interface (often `eth0`, `ens3`, or `wlan0`) and the `inet` line under it, which holds your IPv4 address. To skip the noise and jump straight to the answer: ```bash hostname -I ``` The capital `-I` prints all assigned IP addresses on one line, space-separated. It's the fastest way to grab your local IP, though on a machine with several interfaces you'll get several addresses back. ## The older ifconfig You'll still see `ifconfig` in older tutorials: ```bash ifconfig ``` It's deprecated and not installed by default on recent Ubuntu, but it still works if `net-tools` is present (`sudo apt install net-tools`). I'd stick with `ip` on anything modern. ## Finding your public IP None of the above shows your public address when you're behind a router. For that, you ask an external service what it sees. `curl` makes this trivial: ```bash curl ifconfig.me ``` That hits a service which simply echoes back the IP your request came from. There are several of these; another reliable one is ipify: ```bash curl -s https://api.ipify.org ``` The `-s` flag runs curl silently, so you don't get a progress bar mixed into the output. Both print your public IPv4 address and nothing else. ## Quick reference - Local IP, full detail: `ip addr` - Local IP, one line: `hostname -I` - Public IP: `curl ifconfig.me` When I'm SSH'd into a fresh server and need to know how the world reaches it, `curl ifconfig.me` is the one I type without thinking. You can also look it up in your browser with our [what's my IP tool](/ip). ### How to generate SSH keys with ssh-keygen Source: https://rocketeersapp.com/generate-ssh-keys-ssh-keygen Generate SSH keys with ssh-keygen, choose ed25519 over RSA, copy your public key to a server, load it into ssh-agent, and add it to GitHub. ## Why you want SSH keys Typing a password every time you connect to a server gets old fast, and passwords are the weaker link anyway. SSH keys give you a passwordless, far more secure login: you keep a private key on your machine, hand the matching public key to the server, and SSH proves you own the pair without ever sending a secret over the wire. Once they're set up, [connecting to a server](/connect-to-server-ssh-command) and pushing to Git just work. ## Generating a key with ssh-keygen `ssh-keygen` ships with every Linux distro and macOS. The modern, recommended command is: ```bash ssh-keygen -t ed25519 -C "you@example.com" ``` - `-t ed25519` picks the Ed25519 algorithm. It's fast, secure, and produces short keys. Use this unless something old refuses to accept it. - `-C "you@example.com"` adds a comment, usually your email, so you can recognize the key later in a list of authorized keys. If you're stuck talking to ancient hardware or legacy software that doesn't speak Ed25519, fall back to RSA with a large key size: ```bash ssh-keygen -t rsa -b 4096 -C "you@example.com" ``` ## Passphrase or no passphrase `ssh-keygen` asks for a passphrase. My advice: set one. It encrypts the private key on disk, so a stolen laptop doesn't hand over your servers. The minor inconvenience of typing it is solved by `ssh-agent` (below), which remembers it for your session. Press Enter twice for no passphrase only on throwaway or fully automated keys. ## Where the keys live By default the keys land in `~/.ssh/`: - `~/.ssh/id_ed25519` is your **private** key. Never share it, never commit it, never copy it off the machine. - `~/.ssh/id_ed25519.pub` is your **public** key. This is the safe one you give to servers and GitHub. You can view the public key any time: ```bash cat ~/.ssh/id_ed25519.pub ``` ## Copying the public key to a server The cleanest way to authorize your key on a server is `ssh-copy-id`. It appends your public key to the server's `~/.ssh/authorized_keys` for you: ```bash ssh-copy-id user@host ``` It'll ask for your password one last time. After that, `ssh user@host` logs you in with the key. If `ssh-copy-id` isn't available (it's missing on some macOS setups), you can do it manually: ```bash cat ~/.ssh/id_ed25519.pub | ssh user@host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys" ``` If the server still asks for a password or rejects the key, the [SSH permission denied (publickey) guide](/ssh-permission-denied-publickey) walks through the usual causes. ## Adding the key to ssh-agent `ssh-agent` holds your decrypted key in memory so you only type the passphrase once per session. Start it and add your key: ```bash eval "$(ssh-agent -s)" ssh-add ~/.ssh/id_ed25519 ``` On macOS, store the passphrase in the Keychain so it persists across reboots: ```bash ssh-add --apple-use-keychain ~/.ssh/id_ed25519 ``` ## Adding the key to GitHub To push and pull over SSH, GitHub needs your public key. Copy it to your clipboard: ```bash # macOS pbcopy < ~/.ssh/id_ed25519.pub # Linux (X11) xclip -sel clip < ~/.ssh/id_ed25519.pub ``` Then go to GitHub, open **Settings, SSH and GPG keys, New SSH key**, paste it, and save. Verify it works: ```bash ssh -T git@github.com ``` You should see a greeting with your username. If you instead hit `Permission denied (publickey)`, the [GitHub publickey error guide](/github-permission-denied-publickey) covers the fixes. Once this is green, your key handles servers and Git for good. ### How to install the Xcode Command Line Tools on macOS Source: https://rocketeersapp.com/install-xcode-command-line-tools Install the Xcode Command Line Tools on macOS to get git, clang, make and more, plus how to verify, reset, and reinstall them. ## Why you need the Command Line Tools On a fresh Mac, most developer tooling won't work until you install the Xcode Command Line Tools. This is a lightweight package (you don't need the full multi-gigabyte Xcode app) that gives you the compilers and utilities everything else builds on: `git`, `clang`, `make`, `gcc`, header files, and more. It's also a hard prerequisite for Homebrew and countless other dev tools. ## Installing the tools There's one command to remember: ```bash xcode-select --install ``` This pops up a small dialog asking you to confirm. Click **Install**, agree to the license, and macOS downloads and installs the package for you. It takes a few minutes depending on your connection. If the tools are already installed, you'll see a message saying so, which is harmless. ## Verifying the install Once it finishes, confirm where the tools live with `-p` (print path): ```bash xcode-select -p ``` You should get something like `/Library/Developer/CommandLineTools`. To check the individual binaries are on your `PATH`, ask them for their versions: ```bash git --version gcc --version clang --version make --version ``` Each should print a version string. On macOS, `gcc` is actually a wrapper around Apple's `clang`, so don't be surprised if `gcc --version` mentions clang. ## What you get Beyond the headline tools, the package bundles the C/C++ toolchain, `make`, `git`, `svn`, the linker, and the system headers that native extensions compile against. This is why installing Node modules with native bindings, Ruby gems, or Python packages with C extensions all "just work" afterwards. ## Resetting and reinstalling Sometimes a macOS upgrade leaves the tools in a confused state and you get errors like `tool 'xcodebuild' requires Xcode`. The first thing I try is resetting the active developer directory: ```bash sudo xcode-select --reset ``` That points `xcode-select` back at the default location. If the tools are genuinely broken or missing, remove and reinstall them. Delete the folder, then run the installer again: ```bash sudo rm -rf /Library/Developer/CommandLineTools xcode-select --install ``` The `rm -rf` here is safe because that directory is owned entirely by the tools package; reinstalling recreates it cleanly. ## Next steps With the Command Line Tools in place, you've got a working compiler and `git` out of the box. From here the usual next move is installing Homebrew, which depends on exactly these tools, and from there the rest of your stack. If you run into a "command not found" after install, open a new terminal window so it picks up the updated environment. New to the terminal? See [Linux command line basics](/linux-command-line-basics). ### How to sync files and directories with rsync Source: https://rocketeersapp.com/sync-files-rsync-command Learn how to sync files and directories with rsync, including syncing over SSH, dry runs, deleting, excluding, and the trailing-slash gotcha. ## Why rsync instead of scp When I need to move a directory between machines, my first instinct used to be `scp`. It works, but it copies everything every single time. The moment I started syncing a large project folder repeatedly, that got painfully slow. `rsync` is smarter. It compares source and destination and only transfers the parts that actually changed (the deltas). The first sync is a full copy, but every sync after that is fast because it skips files that are already identical. That makes it perfect for backups and deploys. If you only need a one-off file copy, [scp](/copy-files-over-ssh-scp) is still fine, but for anything you run more than once, reach for rsync. ## The basic local sync The command I use most is short: ```bash rsync -av source/ destination/ ``` The flags: - `-a` is archive mode. It's a bundle that preserves permissions, timestamps, symlinks, and ownership, and recurses into subdirectories. This is what you want 95% of the time. - `-v` is verbose, so you see which files are being transferred. Add `-h` if you want human-readable sizes in the output (`-avh`). ## The trailing-slash gotcha This one bites everyone once. A trailing slash on the **source** changes the meaning: ```bash # Copies the CONTENTS of source/ into destination/ rsync -av source/ destination/ # Copies the source FOLDER itself into destination/ (destination/source/) rsync -av source destination/ ``` With the slash, you get `destination/file.txt`. Without it, you get `destination/source/file.txt`. When something ends up one level too deep, this is almost always why. ## Syncing over SSH rsync runs over SSH by default for remote paths, so syncing to a server is straightforward: ```bash rsync -avz -e ssh local/ user@host:/var/www/app/ ``` - `-z` compresses data during transfer, which helps a lot over slower connections. - `-e ssh` sets SSH as the transport. You can pass options here too, like a custom port: `-e "ssh -p 2222"`. Pulling from a server to your machine is just the two paths swapped: ```bash rsync -avz user@host:/var/www/app/ ./backup/ ``` If you haven't set up key-based access yet, see [connecting to a server with SSH](/connect-to-server-ssh-command) first so you're not typing a password on every sync. ## Always dry-run first Before any sync that deletes or overwrites, I run it with `--dry-run` (or `-n`). It shows exactly what would happen without touching a single file: ```bash rsync -av --dry-run source/ destination/ ``` Pair it with `--progress` once you're happy and want to watch a real transfer: ```bash rsync -av --progress source/ destination/ ``` ## Mirroring with --delete By default rsync never removes files from the destination. If you want a true mirror, where files deleted from the source also disappear from the destination, add `--delete`: ```bash rsync -av --delete source/ destination/ ``` This is powerful and dangerous. Run it with `--dry-run` first, every time. A misplaced trailing slash plus `--delete` is how people wipe a backup. ## Excluding files You rarely want to sync everything. Skip `node_modules`, `.git`, and friends with `--exclude`: ```bash rsync -avz --exclude '.git' --exclude 'node_modules' --exclude '*.log' \ ./app/ user@host:/var/www/app/ ``` For longer lists, put patterns in a file and use `--exclude-from`: ```bash rsync -avz --exclude-from='.rsyncignore' ./app/ user@host:/var/www/app/ ``` ## A practical deploy command Putting it together, here's the kind of one-liner I keep around for deploying a build: ```bash rsync -avz --delete --progress \ --exclude '.git' --exclude '.env' \ ./public/ user@host:/var/www/app/public/ ``` Run it once with `--dry-run` appended, confirm the file list, then run it for real. Once you've done that a couple of times, rsync becomes the tool you never think twice about. ### The complete guide to the curl command Source: https://rocketeersapp.com/curl-command-complete-guide A complete guide to the curl command: downloads, following redirects, headers, methods, data, auth, verbose output, and uploading files. ## What curl is `curl` is the Swiss Army knife for talking to anything over a URL from your terminal. HTTP, HTTPS, FTP, and more. I reach for it constantly: testing an API, downloading a file, checking what headers a server returns, debugging a redirect. It's installed by default on macOS and almost every Linux distro. This guide is the reference I wish I'd had when I started. ## The basic GET request With no flags at all, curl fetches a URL and prints the body to your terminal: ```bash curl https://example.com ``` That's a plain GET request. For working with JSON APIs specifically (sending data, parsing responses), I've got a focused walkthrough in [curl POST and GET API requests](/curl-post-get-api-requests). This guide covers the whole toolbox. ## Downloading files To save the response instead of printing it, use `-o` (lowercase) with a filename, or `-O` (uppercase) to keep the remote filename: ```bash curl -o latest.tar.gz https://example.com/releases/v2.tar.gz curl -O https://example.com/releases/v2.tar.gz ``` Add `-#` for a progress bar, or `-s` to silence the progress meter entirely (handy in scripts). ## Following redirects By default, curl does **not** follow redirects. If a URL returns a 301 or 302, you'll just see the redirect response, not the final page. Add `-L` to follow it: ```bash curl -L https://example.com ``` This trips people up constantly when a download "comes back empty". It's almost always a redirect that curl didn't follow. When in doubt, add `-L`. ## Sending headers Use `-H` to add request headers. You can repeat it as many times as you need: ```bash curl -H "Accept: application/json" \ -H "Authorization: Bearer YOUR_TOKEN" \ https://api.example.com/user ``` ## Choosing the HTTP method `-X` sets the method. GET is the default, so you mostly need this for `POST`, `PUT`, `DELETE`, and friends: ```bash curl -X DELETE https://api.example.com/posts/42 ``` Note that when you send data with `-d` (next section), curl switches to POST automatically, so you don't always need `-X POST`. ## Sending data `-d` sends a request body. This implies POST and a form content type: ```bash curl -d "name=ada&role=admin" https://api.example.com/users ``` For JSON, set the header explicitly and pass a JSON string: ```bash curl -X POST https://api.example.com/users \ -H "Content-Type: application/json" \ -d '{"name": "Ada", "role": "admin"}' ``` To read the body from a file instead of inlining it, use `@`: ```bash curl -X POST https://api.example.com/users \ -H "Content-Type: application/json" \ -d @payload.json ``` ## Authentication For HTTP basic auth, `-u` takes `user:password`: ```bash curl -u admin:secret https://api.example.com/private ``` Leave the password off (`-u admin`) and curl prompts for it, which keeps it out of your shell history. Token-based APIs usually want a header instead, via the `Authorization` header shown earlier. ## Inspecting responses To see only the response headers, use `-I` (a HEAD request): ```bash curl -I https://example.com ``` When something's behaving strangely, `-v` (verbose) shows the full exchange: the request line, every header sent and received, and the TLS handshake: ```bash curl -v https://example.com ``` This is my go-to for debugging anything HTTP. If you want the body and the headers together without full verbosity, use `-i` instead. ## The -k flag, and why to avoid it `-k` (or `--insecure`) tells curl to skip TLS certificate verification: ```bash curl -k https://self-signed.local # avoid in production ``` It's tempting when you hit a certificate error, but it disables the protection that stops man-in-the-middle attacks. Use it only against your own local services with self-signed certs. If you're getting an SSL error against a real site, fix the certificate trust instead. The [curl error 60 SSL certificate guide](/curl-60-ssl-certificate-problem-unable-to-get-local-issuer-certificate) explains how. ## Uploading files To upload a file as a multipart form (like a browser form upload), use `-F`: ```bash curl -F "file=@photo.jpg" https://api.example.com/upload ``` To upload raw file contents as the body (common for PUT to object storage), use `-T`: ```bash curl -T backup.tar.gz https://storage.example.com/bucket/backup.tar.gz ``` ## Wrapping up Most real curl commands are just a few of these flags stacked together: a method, a couple of headers, some data, and `-L`. Once those are muscle memory, curl handles almost any "I need to hit this URL" situation you'll run into. On older Ubuntu, a buggy system curl can throw an [error in the HTTP2 framing layer](/error-in-the-http2-framing-layer) — a curl version issue, not your command. ### How to kill the process running on a port in Linux Source: https://rocketeersapp.com/kill-process-on-port-linux Find the PID listening on a port with lsof, ss or netstat, then kill it cleanly, with one-liner shortcuts using kill, fuser and lsof. ## When a port is stuck You try to start your dev server and Linux tells you the port is taken. Before you can free it, you need to find which process is holding the port, get its PID (process ID), and stop it. Here's how I do it. If you hit the classic [address already in use](/address-already-in-use-port) error, this is exactly the fix. ## Find the process listening on the port My favorite is `lsof`, which lists open files (and sockets count as files): ```bash lsof -i :3000 ``` The `-i :3000` filters to the network connection on port 3000. The output's `PID` column is the number you need. If you prefer the modern socket tool, `ss` works too: ```bash ss -ltnp 'sport = :3000' ``` Those flags read as `-l` listening sockets, `-t` TCP, `-n` numeric ports (don't resolve names), `-p` show the owning process. The older `netstat` does the same job if it's installed: ```bash sudo netstat -tulpn | grep :3000 ``` Here `-t` is TCP, `-u` UDP, `-l` listening, `-p` process, `-n` numeric. You usually need `sudo` to see PIDs owned by other users. ## Kill the process Once you have the PID, send it a termination signal: ```bash kill 12345 ``` Plain `kill` sends `SIGTERM`, which asks the process to shut down gracefully. That's what you want most of the time. If it ignores you and refuses to die, escalate to `SIGKILL`, which the process cannot catch or ignore: ```bash kill -9 12345 ``` Reach for `-9` only when a normal `kill` doesn't work, because the process won't get a chance to clean up. ## The one-liners I actually use Looking up the PID and typing it back in gets old fast. `lsof -t` prints only the PID, so you can feed it straight into `kill`: ```bash kill $(lsof -t -i:3000) ``` Even shorter, `fuser` can find and kill in a single command: ```bash fuser -k 3000/tcp ``` The `-k` flag kills every process attached to that port, and `3000/tcp` scopes it to TCP port 3000. To send `SIGKILL` instead, add `-9`: ```bash fuser -k -9 3000/tcp ``` ## A quick word of caution Before you kill blindly, glance at what's actually using the port, especially with `sudo`. Killing the wrong PID can take down a database or a service you didn't mean to touch. When in doubt, run `lsof -i :3000` first and read the command name before pulling the trigger. ### How to update Ubuntu from the command line Source: https://rocketeersapp.com/update-ubuntu-command-line Learn the difference between apt update and apt upgrade, how to install all updates, clean up old packages, and upgrade to a new Ubuntu release. ## Update vs upgrade: the part everyone confuses The single most common mix-up with `apt` is thinking `update` installs new software. It doesn't. `apt update` only refreshes the list of available packages, while `apt upgrade` actually installs the newer versions. You almost always run them as a pair. ## Refresh the package lists First, sync the local index of what's available from Ubuntu's repositories: ```bash sudo apt update ``` This downloads nothing but metadata. After it runs, `apt` will often tell you how many packages can be upgraded. Nothing on your system has changed yet. ## Install the available updates Now apply them: ```bash sudo apt upgrade ``` `apt upgrade` installs newer versions of packages you already have. By design it won't remove anything or install brand-new dependencies. To skip the confirmation prompt, add `-y`: ```bash sudo apt upgrade -y ``` ## When upgrade isn't enough: full-upgrade Sometimes an update needs to remove an obsolete package or pull in a new dependency, for example a new kernel. Plain `upgrade` holds those back. `full-upgrade` (the modern name for `dist-upgrade`) allows it: ```bash sudo apt full-upgrade ``` I run this for kernel and major library updates. Both `full-upgrade` and `dist-upgrade` do the same thing. ## Updating a single package If you only want to bump one package, name it after `install`. It upgrades just that package (and its dependencies) to the latest available version: ```bash sudo apt install --only-upgrade nginx ``` ## Clean up afterwards Old kernels and orphaned dependencies pile up over time. Remove what's no longer needed: ```bash sudo apt autoremove ``` This is one of the first things I do when I [reclaim disk space on Ubuntu](/reclaim-diskspace-on-ubuntu), since stale kernels eat into `/boot`. ## Upgrading to a new Ubuntu release Everything above keeps your current release patched. To jump to a newer release, like 22.04 to 24.04, you need a different tool: ```bash sudo do-release-upgrade ``` This is a major upgrade, so first make sure your current system is fully patched and check which release you're on with [how to check your Ubuntu version](/check-ubuntu-version-command-line). Add `-d` only if you specifically want to upgrade to a development release. ## The one-liner I actually use Day to day, I chain the common steps together: ```bash sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y ``` That refreshes, installs everything, and tidies up in a single command. ### How to send GET and POST requests with curl Source: https://rocketeersapp.com/curl-post-get-api-requests Send GET and POST API requests with curl, including JSON bodies, form data, auth headers and bearer tokens, and how to inspect the response. ## Talking to APIs from the terminal When I'm building or debugging an API, `curl` is the first tool I reach for. It lets me fire requests straight from the terminal without opening a browser or a GUI client. Here's how I use it day to day. For the full reference, see my [complete curl guide](/curl-command-complete-guide). ## GET requests A GET request is the default, so you only need the URL: ```bash curl https://api.example.com/users ``` If you need query parameters, just append them to the URL. I like to use `-G` together with `--data-urlencode` so curl handles the encoding for me: ```bash curl -G https://api.example.com/users \ --data-urlencode "search=jane doe" \ --data-urlencode "page=2" ``` ## POST requests To send a POST, use `-X POST` and pass a body with `-d` (or `--data`). The presence of `-d` already implies POST, but I keep `-X POST` for clarity: ```bash curl -X POST https://api.example.com/users \ -d "name=Jane" \ -d "email=jane@example.com" ``` That sends data as `application/x-www-form-urlencoded`, which is the classic HTML form encoding. ## Sending JSON Most modern APIs expect JSON. Set the `Content-Type` header with `-H` and pass the body with `-d`: ```bash curl -X POST https://api.example.com/users \ -H "Content-Type: application/json" \ -d '{"name": "Jane", "email": "jane@example.com"}' ``` I wrap the JSON in single quotes so the double quotes inside survive the shell. For larger payloads, read the body from a file with `-d @payload.json`. ## Auth headers and bearer tokens Authentication is almost always a header. Add as many `-H` flags as you need: ```bash curl https://api.example.com/me \ -H "Authorization: Bearer eyJhbGciOi..." \ -H "Accept: application/json" ``` For HTTP basic auth there's a shortcut, `-u user:password`, which builds the header for you: ```bash curl -u admin:secret https://api.example.com/admin ``` ## Inspecting the response When something isn't behaving, I want to see the status code and headers. Use `-i` to include response headers in the output: ```bash curl -i https://api.example.com/users ``` For the full story, including the request curl sent and the TLS handshake, use `-v` (verbose): ```bash curl -v https://api.example.com/users ``` If `-v` shows a certificate error, I've written up the fix for the most common one: [curl error 60: SSL certificate problem](/curl-60-ssl-certificate-problem-unable-to-get-local-issuer-certificate). ## Saving the response To write the body to a file instead of the terminal, use `-o` with a filename, or `-O` to reuse the remote filename: ```bash curl -o response.json https://api.example.com/users ``` That's enough to test almost any HTTP API. Once you're comfortable with `-X`, `-H`, `-d`, and `-i`/`-v`, the rest is just combining them. ### How to make a script executable with chmod +x Source: https://rocketeersapp.com/make-script-executable-chmod-x Fix the Permission denied error when running a shell script by adding the execute bit with chmod +x, plus the shebang line and chmod 755. ## Why a new script says "Permission denied" You write a script, try to run it, and Linux throws this at you: ```bash $ ./deploy.sh bash: ./deploy.sh: Permission denied ``` Nothing is wrong with your code. By default a freshly created file is readable and writable, but not *executable*. Linux refuses to run a file unless its execute permission bit is set, and that's exactly what's missing here. ## Add the execute bit with chmod +x The fix is one command. `chmod` changes a file's mode, and `+x` adds the execute permission: ```bash chmod +x deploy.sh ``` That `+x` grants execute to the user, group, and others at once (limited by your umask). Now Linux is allowed to run the file. ## Run the script With the bit set, run it by giving its path: ```bash ./deploy.sh ``` The `./` matters. It tells the shell "the script is right here in the current directory". Without it, the shell only searches the directories in your `$PATH`, and your script almost certainly isn't in one of those. ## Don't forget the shebang line The execute bit lets Linux *run* the file, but the file also needs to say *which interpreter* runs it. That's the job of the shebang, the very first line: ```bash #!/bin/bash echo "Deploying..." ``` `#!/bin/bash` tells the kernel to feed this file to Bash. For a more portable script you might use `#!/usr/bin/env bash`, which finds Bash via the `PATH`. Without a shebang, the script may run under whatever shell you happen to be in, which can bite you with subtle syntax differences. ## The chmod 755 equivalent You'll often see numeric permissions instead of the `+x` shorthand: ```bash chmod 755 deploy.sh ``` This sets read, write, and execute for the owner (`7`), and read plus execute for the group and everyone else (`5` and `5`). For most scripts `755` is the sensible default. If a script holds secrets and only you should run it, use `700` instead so the group and others get nothing. ## When to use which For a quick "just let me run this", `chmod +x` is all you need. When you want exact, predictable permissions, reach for the numeric form like `755`. If you want to really understand what those numbers mean and how owner, group, and other fit together, read my deeper guide on [changing file permissions with chmod](/change-file-permissions-chmod-linux). ### How to find files in Linux with the find command Source: https://rocketeersapp.com/find-files-linux-command Locate files in Linux with the find command by name, type, size and modification time, and run actions on the results with -exec or -delete. ## Finding files with find When I need to track down a file and don't know exactly where it lives, `find` is the workhorse. It walks a directory tree recursively and matches files against the conditions you give it. The basic shape is `find `, where the path is where to start searching. ## Find by name Search by filename with `-name`. It matches the exact name, so use shell-style wildcards (quoted, so the shell doesn't expand them first): ```bash find . -name '*.log' ``` If case shouldn't matter, use `-iname` instead. This matches `README`, `readme` and `ReadMe`: ```bash find . -iname 'readme*' ``` ## Find by type Limit results to files or directories with `-type`. Use `f` for regular files and `d` for directories: ```bash find /var/www -type f -name '*.php' find /var/www -type d -name 'cache' ``` ## Find by size The `-size` flag matches on file size. Suffix the number with `k`, `M` or `G`, and prefix it with `+` for "larger than" or `-` for "smaller than": ```bash find . -type f -size +100M ``` This finds files larger than 100 megabytes — handy for hunting down what's eating your disk. ## Find by modification time `-mtime` matches by how many days ago a file was last modified. `-mtime -7` means changed within the last 7 days, while `+30` means older than 30 days: ```bash find /tmp -type f -mtime +30 ``` For finer control, `-mmin` works the same way but in minutes. ## Searching a specific path The path argument can be anything — an absolute path, a relative one, or several at once: ```bash find /etc /opt -name '*.conf' ``` ## Acting on results with -exec Beyond listing matches, `find` can run a command on each one with `-exec`. The `{}` is replaced by the filename and `\;` ends the command: ```bash find . -name '*.tmp' -type f -exec rm {} \; ``` For deleting specifically, the built-in `-delete` is cleaner and avoids the "[argument list too long](/argument-list-too-long)" error you'd hit with `rm *`: ```bash find ./logs -name '*.log' -type f -delete ``` You can chain `find` with other tools too. To search the contents of every matching file, pipe the names into [grep](/search-files-grep-command): ```bash find . -name '*.php' -exec grep -l 'TODO' {} \; ``` Combine conditions freely — they're ANDed together — and `find` becomes a precise way to locate exactly the files you're after. It pairs well with the rest of the [Linux command line basics](/linux-command-line-basics). ### How to create and extract tar archives in Linux Source: https://rocketeersapp.com/create-extract-tar-archives-linux Create, compress, list and extract tar archives in Linux — including .tar.gz files, extracting to a directory and pulling out a single file from an archive. ## Working with tar archives `tar` bundles many files and directories into a single archive file, optionally compressed. I use it constantly for backups, releases and moving directories between servers. The flags look cryptic at first, but they're just letters you combine. The ones you'll use most: - `c` — create a new archive - `x` — extract an archive - `t` — list the contents - `v` — verbose, print each file as it's processed - `f` — the next argument is the archive filename (always needed) - `z` — pass the archive through gzip compression ## Create an archive Bundle a directory into a `.tar` file with `c`, `v` and `f`: ```bash tar -cvf backup.tar ./project ``` This creates `backup.tar` containing the `project` directory, printing each file as it goes. ## Create a compressed .tar.gz Add `z` to gzip the archive as it's built. The convention is to name it `.tar.gz` (or `.tgz`): ```bash tar -czvf backup.tar.gz ./project ``` Compressed archives are much smaller and the standard choice for backups or transferring over the network. ## Extract an archive Swap `c` for `x` to extract. For an uncompressed archive: ```bash tar -xvf backup.tar ``` For a gzip-compressed one, add `z` again: ```bash tar -xzvf backup.tar.gz ``` Modern versions of `tar` actually detect the compression automatically, so `tar -xvf backup.tar.gz` usually works too — but being explicit with `z` never hurts. ## Extract to a specific directory By default files land in the current directory. Use `-C` to extract somewhere else (the target directory must already exist): ```bash tar -xzvf backup.tar.gz -C /var/www/releases ``` ## List the contents Before extracting an archive someone handed you, it's worth peeking inside with `t`. This lists every entry without unpacking anything: ```bash tar -tvf backup.tar.gz ``` ## Extract a single file You don't have to unpack the whole thing. Name the exact path (as shown by the listing above) after the archive to pull out just that file: ```bash tar -xzvf backup.tar.gz project/config/app.php ``` This recreates only `project/config/app.php` relative to your current directory. ## Putting it together Once the letters click, `tar` reads naturally: `-czvf` is "create, gzip, verbose, file" and `-xzvf` is "extract, gzip, verbose, file". Those two cover the large majority of what you'll ever need. ### How to connect to a server with the ssh command Source: https://rocketeersapp.com/connect-to-server-ssh-command Connect to a remote server with the ssh command, including custom ports, key files, the host key prompt, SSH config aliases, and verbose debugging. ## Connecting with ssh SSH is how I reach almost every server I work with. The basic form is the command, the user, and the host: ```bash ssh jane@203.0.113.10 ``` You can use a hostname instead of an IP, and SSH will resolve it: ```bash ssh jane@app.example.com ``` If you don't pass a key explicitly, SSH tries the default keys in `~/.ssh`. If you don't have a key yet, generate one first: [create SSH keys with ssh-keygen](/generate-ssh-keys-ssh-keygen). Once you're in, you can [run a single command over SSH](/run-command-over-ssh) or [forward a port through the tunnel](/ssh-tunnel-port-forwarding); and if you don't yet know the server's address, here's [how to find its IP](/find-ip-address-linux-command-line). ## Custom port SSH listens on port 22 by default. Many servers move it elsewhere for security. Use `-p` (lowercase) to specify the port: ```bash ssh -p 2222 jane@app.example.com ``` ## Using a specific key file Cloud providers often hand you a `.pem` file. Point SSH at it with `-i` (identity file): ```bash ssh -i ~/.ssh/aws-key.pem ubuntu@203.0.113.10 ``` If the key has loose permissions, SSH refuses to use it. Fix it with `chmod 400 ~/.ssh/aws-key.pem` so only you can read it. ## The first-connection prompt The first time you connect to a host, SSH shows the server's fingerprint and asks: ```bash The authenticity of host 'app.example.com' can't be established. ED25519 key fingerprint is SHA256:abc123... Are you sure you want to continue connecting (yes/no/[fingerprint])? ``` Type `yes` and SSH stores the fingerprint in `~/.ssh/known_hosts`. On later connections it checks silently. This is expected; it only warns you again if the fingerprint changes. ## Host aliases with ~/.ssh/config Typing long commands gets old fast. I keep a `~/.ssh/config` so I can connect with a short alias: ```bash Host prod HostName app.example.com User jane Port 2222 IdentityFile ~/.ssh/aws-key.pem ``` Now the whole thing collapses to: ```bash ssh prod ``` Every flag above can live in the config, which keeps it out of your muscle memory and your shell history. ## Debugging with -v When a connection misbehaves, `-v` (verbose) shows exactly what SSH is doing: which keys it offers, what the server accepts, and where it stops. Add more `v`s for more detail: ```bash ssh -v prod ssh -vvv prod ``` If you hit `Permission denied (publickey)`, I've written up the causes and fixes here: [SSH permission denied (publickey)](/ssh-permission-denied-publickey). ## Where to go next Once you can connect, you can do more than open a shell. Run a single command without a full session with [run a command over SSH](/run-command-over-ssh), and move files with [scp over SSH](/copy-files-over-ssh-scp). ### Getting started with the AWS CLI Source: https://rocketeersapp.com/getting-started-aws-cli Install the AWS CLI v2 on macOS and Linux, run aws configure, set up named profiles, and try real S3 and EC2 commands from your terminal. ## What the AWS CLI is for Clicking through the AWS Console is fine for poking around, but the moment you want to script anything, list buckets quickly, or copy files to S3, the AWS CLI is the tool. It talks to every AWS service from your terminal, which means you can automate it and put it in deploy scripts. Here's how I get it running on a fresh machine. ## Installing the AWS CLI v2 Always install **v2**. Version 1 still floats around in package managers but is no longer the one you want. On macOS, use the official installer: ```bash curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg" sudo installer -pkg AWSCLIV2.pkg -target / ``` On Linux (x86_64): ```bash curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install ``` On an ARM box (like a Graviton instance or some Raspberry Pis), swap the URL for `awscli-exe-linux-aarch64.zip`. Confirm it landed: ```bash aws --version ``` You should see something like `aws-cli/2.x.x`. ## Configuring your credentials Before anything works, the CLI needs an access key. Create one under **IAM, Users, Security credentials** in the Console, then run: ```bash aws configure ``` It asks four questions: - **AWS Access Key ID** — your key, like `AKIA...` - **AWS Secret Access Key** — the secret half (shown only once when created) - **Default region name** — e.g. `eu-west-1` or `us-east-1` - **Default output format** — `json`, `table`, or `text`. I use `json`. ## Where credentials are stored `aws configure` writes two plain-text files to `~/.aws/`: - `~/.aws/credentials` holds your keys. - `~/.aws/config` holds the region and output format. Because these are plain text, treat `~/.aws/credentials` like a private SSH key: never commit it, and lock down its permissions if you're unsure. You can inspect the configured values without opening the files: ```bash aws configure list ``` ## Named profiles for multiple accounts The moment you deal with more than one AWS account (work and personal, staging and production), named profiles save you. Set one up: ```bash aws configure --profile staging ``` Then pass `--profile` on any command: ```bash aws s3 ls --profile staging ``` You can also export it for a whole shell session so you don't repeat yourself: ```bash export AWS_PROFILE=staging ``` ## A few real commands List your S3 buckets: ```bash aws s3 ls ``` List the contents of one bucket, then copy a file up to it: ```bash aws s3 ls s3://my-bucket/ aws s3 cp ./backup.tar.gz s3://my-bucket/backups/ ``` The `aws s3 cp` command works just like a normal `cp`, including `--recursive` for whole directories. To sync a folder (only changed files), there's `aws s3 sync`, which behaves a lot like [rsync](/sync-files-rsync-command). Check your EC2 instances: ```bash aws ec2 describe-instances ``` That dumps a lot of JSON. Narrow it down with the built-in `--query` (JMESPath) to pull just what you need: ```bash aws ec2 describe-instances \ --query "Reservations[].Instances[].{ID:InstanceId,State:State.Name}" \ --output table ``` ## Where to go next From here, almost everything is `aws `, and `aws help` documents each one. Tab completion is worth setting up too. With credentials configured and v2 installed, you've got the foundation for scripting anything AWS exposes. Rocketeers connects to [AWS](/providers/aws) as a provider, too, so you can manage it alongside your other clouds. ### Essential Linux command line basics for developers Source: https://rocketeersapp.com/linux-command-line-basics A practical primer on Linux command line basics: navigating, working with files, pipes and redirection, permissions, and where to go next. ## Why the command line is worth it If you write code or touch a server, the terminal is unavoidable, and honestly that's a good thing. It's faster than clicking, it's scriptable, and it's the same on your laptop and on a remote box. This is the set of commands I'd hand a new developer to get comfortable. Everything here works on Ubuntu and macOS alike. ## Knowing where you are and moving around The terminal always has a "current directory". Three commands cover navigation: ```bash pwd # print working directory: where am I right now? ls # list files in the current directory cd /var/www # change directory ``` `ls` becomes much more useful with flags: ```bash ls -l # long format: permissions, owner, size, date ls -la # also show hidden files (the ones starting with a dot) ls -lh # human-readable sizes (KB, MB instead of bytes) ``` A few `cd` shortcuts save constant typing: `cd ~` goes home, `cd ..` goes up one level, and `cd -` jumps back to the previous directory. ## Working with files and directories The everyday file commands: ```bash mkdir project # make a directory touch project/app.js # create an empty file (or update its timestamp) cp app.js app.bak.js # copy a file mv app.bak.js backup/ # move or rename a file rm app.bak.js # remove a file ``` Be careful with `rm`. There is no recycle bin. To delete a directory and everything in it you need `rm -r directory`, and that's exactly the command people regret. Double-check the path before you hit Enter. To read files without opening an editor: ```bash cat app.js # dump the whole file to the screen less app.js # scroll through a long file (q to quit) head -n 20 app.js # first 20 lines tail -n 20 app.js # last 20 lines tail -f app.log # follow a log file live ``` ## Pipes and redirection This is where the command line gets powerful. The pipe `|` sends one command's output into another command's input: ```bash ls -l | less # page through a long listing cat app.log | grep ERROR # show only lines containing ERROR ``` Redirection sends output to a file instead of the screen: ```bash echo "hello" > notes.txt # write to a file (OVERWRITES it) echo "more" >> notes.txt # append to a file (keeps existing content) ``` The difference between `>` and `>>` matters: a single `>` wipes the file first. Get them mixed up and you'll lose the contents of a file you meant to add to. ## A quick word on permissions Run `ls -l` and you'll see strings like `-rwxr-xr--` at the start of each line. Those are permissions: read (`r`), write (`w`), and execute (`x`), shown for the owner, the group, and everyone else. When a script "won't run" or you get a "permission denied", this is usually why. The full story, including `chmod` and the numbers like `755`, is in the [change file permissions with chmod guide](/change-file-permissions-chmod-linux). ## Where to go next These basics get you around, but the real productivity comes from a handful of focused tools: - Finding files by name or type: [find files on the Linux command line](/find-files-linux-command). - Searching inside files for text: [search files with grep](/search-files-grep-command). - Bundling and compressing directories: [create and extract tar archives](/create-extract-tar-archives-linux). - Working on a remote machine: [connect to a server with SSH](/connect-to-server-ssh-command). Pick one command, use it for real on something you're actually doing, and it sticks. That beats memorizing a cheat sheet every time. ### How to check your Ubuntu version from the command line Source: https://rocketeersapp.com/check-ubuntu-version-command-line A quick guide to checking which Ubuntu release and kernel you're running, using lsb_release, /etc/os-release, hostnamectl and uname. ## Why you need to know your Ubuntu version Before I install a package, follow a tutorial, or open a support ticket, I want to know exactly which Ubuntu release I'm on. The version decides which package versions are available, which docs apply, and whether a release is still getting security updates. Here are the commands I reach for. ## The quickest answer: lsb_release My go-to is `lsb_release`, which prints the distributor info in a clean format: ```bash lsb_release -a ``` The `-a` flag means "all". You'll get the distributor ID, a description like `Ubuntu 24.04.2 LTS`, the release number (`24.04`), and the codename (`noble`). If you only want the number, use `-r`, and `-c` gives just the codename: ```bash lsb_release -r lsb_release -c ``` ## Reading /etc/os-release `lsb_release` isn't installed on every minimal image, but `/etc/os-release` always is. It's a plain file you can just print: ```bash cat /etc/os-release ``` This shows `VERSION_ID`, `VERSION_CODENAME`, and `PRETTY_NAME`. Because it's structured as shell variables, scripts can source it directly, which makes it handy in automation. ## The old-school /etc/issue For a fast glance, `/etc/issue` holds the text shown before login: ```bash cat /etc/issue ``` It prints something like `Ubuntu 24.04.2 LTS \n \l`. The `\n` and `\l` are placeholders the login prompt fills in, so just ignore them. ## A richer view with hostnamectl `hostnamectl` gives the OS plus the hostname, architecture, and kernel in one go: ```bash hostnamectl ``` I like this one on servers because the "Operating System" and "Kernel" lines tell me almost everything at a glance. ## Checking the kernel version The Ubuntu release and the kernel are separate things. To see the running kernel, use `uname`: ```bash uname -r ``` The `-r` flag prints just the kernel release, like `6.8.0-52-generic`. Want the full picture, including architecture and build date? Use `-a`: ```bash uname -a ``` ## Which one should you use? For everyday use, `lsb_release -a` is the friendliest. On a stripped-down container or cloud image where it's missing, fall back to `cat /etc/os-release`. And when you're chasing a driver or hardware issue, `uname -r` is the kernel detail that actually matters. Once you know your version, you'll likely want to [update Ubuntu from the command line](/update-ubuntu-command-line) to pull in the latest security patches. While you're sizing up a server, also check [how much RAM it has](/how-much-memory-on-ubuntu) and [how many CPU cores](/how-many-cpu-cores-on-ubuntu). See also the quick [Ubuntu version commands](/ubuntu-version). ### How to search file contents with grep Source: https://rocketeersapp.com/search-files-grep-command Search inside files with grep — recursive search, case-insensitive matching, line numbers, context, regex and piping output from other commands into grep. ## Searching inside files with grep While [find](/find-files-linux-command) locates files by their name or metadata, `grep` searches what's *inside* them. It scans for lines matching a pattern and prints them. The basic form is `grep 'pattern' file`: ```bash grep 'database' config/app.php ``` ## Search recursively To search every file under a directory, add `-r` (recursive). This is the one I use most: ```bash grep -r 'API_KEY' . ``` ## Case-insensitive matching By default grep is case-sensitive. Add `-i` to ignore case, so `Error`, `error` and `ERROR` all match: ```bash grep -ri 'todo' src/ ``` ## Show line numbers Add `-n` to prefix each match with its line number, which makes jumping to it in an editor much faster: ```bash grep -n 'function' app.js ``` ## Match whole words and invert Use `-w` to match a whole word only, so searching `log` won't also match `login` or `catalog`: ```bash grep -w 'log' app.php ``` Flip the logic with `-v` to print lines that *don't* match — useful for filtering noise out of a file: ```bash grep -v '^#' config.ini ``` That example drops every commented line (those starting with `#`). ## Count matches When you only care how many times something appears, `-c` prints the count of matching lines instead of the lines themselves: ```bash grep -c 'POST' access.log ``` ## Show surrounding context Sometimes the matching line alone isn't enough. Use `-A` for lines after, `-B` for before, and `-C` for both: ```bash grep -C 3 'Exception' storage/logs/laravel.log ``` This prints each match with 3 lines on either side. ## Using regular expressions The pattern is a regular expression, so you can match more than literal text. Add `-E` for extended regex syntax like alternation and `+`: ```bash grep -E 'warning|error|critical' app.log ``` This finds any line containing one of those three words. ## Piping into grep grep really shines at the end of a pipe, filtering the output of another command. This is everyday glue on the command line: ```bash ps aux | grep nginx git log --oneline | grep 'fix' ``` Combine these flags freely — for example `grep -rin 'token' .` gives you a recursive, case-insensitive search with line numbers — and grep becomes one of the most useful tools in your kit. For the wider toolkit, see [Linux command line basics](/linux-command-line-basics). ### How to copy files over SSH with scp Source: https://rocketeersapp.com/copy-files-over-ssh-scp Copy files and directories between your machine and a remote server over SSH using scp, including custom ports, key files and host-to-host transfers. ## Copying files over SSH Whenever I need to move a file to or from a server, `scp` is the tool I reach for first. It rides on top of SSH, so if you can already [connect to the server over SSH](/connect-to-server-ssh-command), you can copy files to it without any extra setup. The general syntax is `scp `, where a remote location looks like `user@host:/path`. ## Local to remote To upload a file from your machine to a server, put the remote location on the right: ```bash scp ./backup.sql deploy@example.com:/var/www/backups/ ``` This copies `backup.sql` into the `/var/www/backups/` directory on the server. If you want to rename it on the way, just include a filename in the destination path. ## Remote to local Flip the arguments to download instead. Here the remote path is the source, and `.` is the current local directory: ```bash scp deploy@example.com:/var/log/nginx/error.log . ``` ## Copying directories with -r By default `scp` only handles single files. To copy a whole directory and everything inside it, add `-r` (recursive): ```bash scp -r ./public deploy@example.com:/var/www/app/ ``` ## Using a custom port with -P If the server's SSH daemon listens on a non-standard port, pass it with `-P` (a capital P — lowercase `-p` preserves timestamps instead): ```bash scp -P 2222 ./config.yml deploy@example.com:/etc/app/ ``` ## Using a specific key with -i When the server expects a particular private key, point at it with `-i`. This is handy when you juggle several keys: ```bash scp -i ~/.ssh/deploy_key ./release.tar.gz deploy@example.com:/tmp/ ``` If you'd rather not type this every time, you can configure the key per host in `~/.ssh/config` and `scp` will pick it up automatically. ## Copying between two remote hosts You don't even need the file to pass through your machine. Give two remote locations and `scp` copies directly between them: ```bash scp deploy@host1.example.com:/data/dump.sql deploy@host2.example.com:/data/ ``` Add `-3` if the two hosts can't reach each other directly and you want the transfer routed through your local machine instead. ## A few tips You can combine flags, for example `scp -rP 2222` to recursively copy over a custom port. And if you're moving large directories around regularly, `rsync` is often a better fit since it only transfers what changed — but for a quick one-off copy, `scp` is hard to beat. ### How to run a command on a remote server over SSH Source: https://rocketeersapp.com/run-command-over-ssh Run one-off commands on a remote server over SSH without an interactive session, including quoting, sudo, capturing output and running local scripts remotely. ## Running a command without logging in Most of the time when I SSH into a server I just want to run a single command and get out. Instead of starting an interactive session, you can pass the command straight to `ssh` and it runs there, prints the output locally and disconnects. This builds on a working SSH setup, so if you can [connect to the server over SSH](/connect-to-server-ssh-command), this works out of the box. The pattern is `ssh user@host 'command'`: ```bash ssh deploy@example.com 'uptime' ``` ## Quoting matters Always wrap the command in single quotes. Without them, your local shell may expand variables and globs before they ever reach the server. Compare these two: ```bash ssh deploy@example.com 'echo $HOME' # prints the server's $HOME ssh deploy@example.com "echo $HOME" # prints your local $HOME ``` Use single quotes when you want the remote shell to interpret the command. ## Running multiple commands Chain commands with `&&` (stop on failure) or `;` (always continue), all inside the same quotes: ```bash ssh deploy@example.com 'cd /var/www/app && git pull && composer install' ``` ## Using sudo over SSH Running `sudo` non-interactively needs a terminal for the password prompt. Add `-t` to force one to be allocated: ```bash ssh -t deploy@example.com 'sudo systemctl restart nginx' ``` The `-t` flag gives `sudo` a proper TTY so it can ask for your password. ## Capturing output locally Because the output comes back over the connection, you can redirect or pipe it on your own machine like any other command: ```bash ssh deploy@example.com 'cat /var/log/syslog' > server-syslog.txt ssh deploy@example.com 'df -h' | grep '/dev/sda1' ``` ## Running a local script on the server A neat trick: you can feed a script from your machine into a remote shell without copying it over first. Pipe it into `bash -s`: ```bash ssh deploy@example.com 'bash -s' < ./provision.sh ``` The remote `bash -s` reads the script from standard input, which `ssh` pipes from your local file. You can even pass arguments to the script after `bash -s`: ```bash ssh deploy@example.com 'bash -s' -- arg1 arg2 < ./provision.sh ``` This is great for ad-hoc provisioning or maintenance scripts you keep locally but want to run on several servers without uploading them each time. ### How to forward ports with SSH tunneling Source: https://rocketeersapp.com/ssh-tunnel-port-forwarding Forward ports over SSH with local, remote, and dynamic SOCKS tunnels, including reaching a remote MySQL database safely from your laptop. ## What SSH tunneling is for SSH can do more than give you a shell: it can carry other connections through its encrypted channel. I lean on this constantly to reach services that aren't exposed to the internet, like a database bound to `localhost` on a remote server. If you're new to the `ssh` command itself, start with [connecting to a server with ssh](/connect-to-server-ssh-command). There are three kinds of forwarding worth knowing. ## Local forwarding (-L) Local forwarding makes a remote service appear on a port of your own machine. The syntax is `-L local_port:target_host:target_port`: ```bash ssh -L 8080:localhost:80 jane@app.example.com ``` Here `localhost:80` is resolved **from the server's perspective**, so this exposes the server's local web service on `http://localhost:8080` on your laptop. The classic use is a database. Say MySQL on the server listens on `3306` but isn't reachable from outside. Tunnel it: ```bash ssh -L 3306:localhost:3306 jane@app.example.com ``` Now point your local MySQL client at `127.0.0.1:3306` and you're talking to the remote database over SSH. If `3306` is already busy locally, just pick another local port like `-L 13306:localhost:3306`. ## Remote forwarding (-R) Remote forwarding is the reverse: it exposes a port on **your** machine to the server. The syntax is `-R remote_port:target_host:target_port`. This is handy for letting a remote box reach a service running on your laptop, for example a webhook receiver during development: ```bash ssh -R 9000:localhost:3000 jane@app.example.com ``` Now connections to port `9000` on the server are forwarded to your local port `3000`. ## Dynamic forwarding (-D) Dynamic forwarding turns SSH into a local SOCKS proxy, routing whatever you send through it out via the server. Point a browser or tool at the SOCKS port and your traffic exits from the server: ```bash ssh -D 1080 jane@app.example.com ``` Configure your application to use `127.0.0.1:1080` as a SOCKS5 proxy. This is great for reaching internal services that are only allowed from the server's network. ## The -N and -f flags When you're tunnelling, you usually don't want an interactive shell as well. Two flags help: - `-N` tells SSH not to run a remote command (just forward), so you don't get a shell prompt. - `-f` sends SSH to the background after authenticating. Combined, they give you a clean background tunnel: ```bash ssh -fN -L 3306:localhost:3306 jane@app.example.com ``` That command sets up the MySQL tunnel and returns your prompt immediately. To shut it down later, find and kill it: ```bash pkill -f "3306:localhost:3306" ``` Once you've got the `-L`, `-R`, and `-D` shapes memorised, you can punch a secure path to almost any service without exposing it to the open internet. ### How to change file permissions with chmod Source: https://rocketeersapp.com/change-file-permissions-chmod-linux Change Linux and macOS file permissions with chmod using symbolic and numeric modes, recursive changes, and why 777 is rarely the answer. ## How Linux permissions work Every file and directory on Linux and macOS has permissions for three groups: the **user** (owner), the **group**, and **others** (everyone else). Each group can have three permissions: **read** (`r`), **write** (`w`), and **execute** (`x`). Run `ls -l` and you'll see them on the left: ```bash ls -l -rw-r--r-- 1 jane staff 1240 Jun 23 10:00 notes.txt ``` The first character is the file type (`-` for a file, `d` for a directory). The next nine are three blocks of `rwx`: owner, group, others. Above, the owner can read and write, while group and others can only read. `chmod` ("change mode") is how you adjust those bits. ## Symbolic mode Symbolic mode is the readable way to make targeted changes. You name the target (`u` user, `g` group, `o` others, `a` all), then `+`, `-`, or `=`, then the permission. ```bash chmod u+x deploy.sh # give the owner execute chmod g-w report.txt # remove write from the group chmod o=r config.ini # set others to read-only exactly chmod a+r public.txt # give everyone read ``` The `+` adds, `-` removes, and `=` sets exactly (clearing anything not listed). This is my go-to when I just need to flip one bit. Making a script runnable is so common I gave it its own write-up: [make a script executable with chmod +x](/make-script-executable-chmod-x). ## Numeric (octal) mode Numeric mode sets all three groups at once. Each digit is the sum of read (`4`), write (`2`), and execute (`1`): - `7` = `rwx` (4+2+1) - `6` = `rw-` (4+2) - `5` = `r-x` (4+1) - `4` = `r--` The three digits are owner, group, others, in that order: ```bash chmod 755 deploy.sh # rwxr-xr-x — owner full, others read+execute chmod 644 notes.txt # rw-r--r-- — owner read+write, others read chmod 600 secret.key # rw------- — owner only ``` `755` is the standard for scripts and directories; `644` is standard for regular files. ## Recursive changes To apply a mode to a directory and everything inside it, add `-R`: ```bash chmod -R 755 ./public ``` Be careful here. Files and directories usually want different permissions (directories need execute to be enterable), so a blanket recursive chmod can break things. When that matters I use `find` to target each type separately: ```bash find ./public -type d -exec chmod 755 {} \; find ./public -type f -exec chmod 644 {} \; ``` ## A word on 777 You'll see `chmod 777` suggested as a quick fix all over the internet. It grants read, write, and execute to **everyone**, which is almost never what you want. On a shared server it's a real security risk. If something isn't working, the fix is usually correct ownership (`chown`) or a sensible `644`/`755`, not throwing the doors wide open. ### How to install Composer packages locally Source: https://rocketeersapp.com/install-composer-packages-locally ## Developing Composer packages locally During development I regularly need to install Composer packages from a Git repository on my local machine. When your developing a Composer package, you usually want to see the changes you've made to a package directly when using or testing it on a PHP project. This can be done using the symlink feature, where Composer is instructed to symlink the repository from your local drive. This also means you have to make sure the package is in the same folder as the PHP project your installing the package to. Sometimes I need to do this multiple times a day. Because it is a little anoying to do this every time manually, while also tending to forget the JSON structure that's needed. That's why I put in the effort to create a sweet Bash function (or alias) for this. ## What does it do The bash function handles some common situations gracefully: - It prevents exection in the wrong directory (a folder without composer.json) - It removes the current vendor folder and composer.lock file to prevent conflicted packages when installing the local package - It returns the changes on files done by the alias back to its prior state, so you won't commit the wrong composer.json or composer.lock file by accident Bottom line, it takes the pain away of setting it all up manually! ## The bash function The function checks for an existing composer.json, leverages the composer binary to change the configuration of the composer.json, removes the current vendor and composer.lock to discard any conflicting situations, requires the package to install it using the configured symlink and finally removes the changes made from the Git repository like it's never been changed. But by still keeping the `vendor` folder in tact, the package will be symlinked locally. ```bash package() { if [[ ! -f "composer.json" ]]; then echo "composer.json not found" exit 1 fi VENDOR="${2:-vormkracht10}" composer config repositories.$1 '{"type": "path", "url": "../'$1'", "options": {"symlink": true}}' --file composer.json rm -Rf ./vendor rm -Rf ./composer.lock composer require $VENDOR/$1 @dev git checkout -- composer.json git checkout -- composer.lock } ``` ## Usage You can use the Bash function in two ways: You can change the `vormkracht10` string to the vendor or organization of your preference. When you're needing packages mostly from this vendor, it keeps the command nice and short: ```bash package [package-name] # e.g. package laravel-ok ``` If you do need to overwrite the vendor, you can do this by setting the second parameter to a custom vendor: ```bash package [package-name] [vendor] # e.g. package laravel-ok vormkracht10 ``` Working with Composer more broadly? See [how to install PHP](/how-to-install-php), which includes Composer, and the fix for [Composer out of memory](/composer-out-of-memory-allowed-memory-size-exhausted). ### Argument list too long (Bash: /bin/rm) Source: https://rocketeersapp.com/argument-list-too-long ## The error and problem Sometimes you can encounter this error when executing a `rm ./directory/*` inside a directory to clean it up: ```bash $ bash: /bin/rm: Argument list too long ``` ## Why this happens This is because of the `ARGS_MAX` setting in your operating systems config, which defines the maximum argument size a command can accept. The solution is to not use a wildcard in your command: ```bash rm ./directory ``` But the downside to this, is that you're now deleting the directory as well. And most of the times you don't want this. Because then you need to recreate the directory again and you have to take in account of the persmissions the directory previously had. ## The solution The solution is to use the `find` command, which can perform actions directly inside a directory and only on the files. This way you don't need to use a wildcard (which would introduce the same problem): ```bash find ./directory -type f -delete ``` You can even make it more specific to only delete certain files, in this example to only delete log files: ```bash find ./directory -name '*.log' -type f -delete ``` ## Databases ### How to install MySQL on Ubuntu Source: https://rocketeersapp.com/how-to-install-mysql Install the latest MySQL on Ubuntu, secure it, and create your first database and user — the right way, from the official MySQL repository. Ubuntu ships MySQL in its repositories, but it's often an older release. To run a current, supported MySQL, install it from MySQL's own apt repository. Here's the full path from a fresh server to a database your application can connect to. ## Add the official MySQL repository Import MySQL's signing key and add the repository for the current LTS release: ```bash sudo apt-get install -y dirmngr gnupg echo "deb http://repo.mysql.com/apt/ubuntu $(lsb_release -sc) mysql-8.0-lts" \ | sudo tee /etc/apt/sources.list.d/mysql.list ``` Pin it so apt prefers MySQL's packages over Ubuntu's: ```bash printf 'Package: mysql*\nPin: origin repo.mysql.com\nPin-Priority: 1001\n' \ | sudo tee /etc/apt/preferences.d/mysql ``` ## Install MySQL ```bash DEBIAN_FRONTEND=noninteractive sudo apt-get update DEBIAN_FRONTEND=noninteractive sudo apt-get install -y mysql-server mysql-client ``` Start it and enable it on boot: ```bash sudo systemctl enable --now mysql ``` ## Secure the installation A fresh MySQL has test databases, anonymous access, and a passwordless root. Lock it down: ```bash sudo mysql_secure_installation ``` Set a strong root password, remove the anonymous users, disallow remote root login, and drop the test database when prompted. ## Create a database and user Never let your application connect as root. Open a MySQL prompt: ```bash sudo mysql ``` Then create a dedicated database and user with privileges only on that database: ```sql CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'myapp'@'localhost' IDENTIFIED BY 'a-strong-password'; GRANT ALL PRIVILEGES ON `myapp`.* TO 'myapp'@'localhost'; FLUSH PRIVILEGES; ``` If you hit `ERROR 1045 Access denied`, the username, password, or host doesn't match what you granted — see [access denied for user](/sqlstate-hy000-1045-access-denied-for-user). ## Allow remote connections (only if you need them) By default MySQL listens on `localhost` only, which is the safest setting when the database and app share a server. If a separate application server needs access, bind to all interfaces in a config file: ```ini # /etc/mysql/conf.d/networking.cnf [mysqld] bind-address = 0.0.0.0 ``` Then create the user with the `'%'` host and restart MySQL: ```sql CREATE USER 'myapp'@'%' IDENTIFIED BY 'a-strong-password'; GRANT ALL PRIVILEGES ON `myapp`.* TO 'myapp'@'%'; FLUSH PRIVILEGES; ``` Only do this behind a firewall that restricts which IPs can reach port 3306. An open MySQL port is one of the fastest ways to get a server compromised. ## Next steps Now you can [import a database from the command line](/import-database-mysql-command-line), [export one](/export-database-mysql-command-line), and start thinking about [backups](/backup-mysql-databases-single-file) and [performance tuning](/optimize-mysql-performance). On a busy server you'll eventually meet [too many connections](/mysql-1040-too-many-connections) — worth knowing about before it happens. Prefer Postgres? See [installing PostgreSQL with pgvector](/install-postgresql-pgvector-ubuntu). ## Let Rocketeers handle it Adding the right repository, pinning packages, securing the defaults, raising file-descriptor limits, and creating databases and scoped users by hand is a lot of careful steps to get exactly right — and easy to leave half-done. Rocketeers installs and secures MySQL during provisioning and creates databases and users on demand, with credentials stored safely, so you skip straight to using the database. ### MySQL ERROR 1205: Lock wait timeout exceeded Source: https://rocketeersapp.com/mysql-1205-lock-wait-timeout-exceeded This error means a transaction waited too long for a row lock held by another transaction and gave up. Usually a long-running or stuck transaction is holding the lock. ## About the error ```bash SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction ``` InnoDB uses row-level locks. When one transaction holds a lock on a row and another wants it, the second waits, up to `innodb_lock_wait_timeout` (default 50 seconds). If the lock isn't released in time, the waiting transaction is rolled back with error 1205. ## Why do I see this error - A **long-running transaction** holding locks while it does slow work (or waits on something external). - A transaction that was **never committed or rolled back**, left open by a crashed process or an interactive session. - High contention: many requests updating the same rows at once. - Deadlock-adjacent patterns where transactions acquire locks in different orders. ## Solution ### Find the blocking transaction See which transactions are running and how long they've been open: ```sql SELECT * FROM information_schema.innodb_trx\G ``` On MySQL 8 you can see exactly who blocks whom: ```sql SELECT * FROM sys.innodb_lock_waits\G ``` If you find a transaction stuck open, kill it by its thread id: ```sql KILL ; ``` ### Keep transactions short The durable fix is to hold locks for as little time as possible. Don't do slow work inside a transaction, no external API calls, no waiting on user input, no heavy computation between `BEGIN` and `COMMIT`. In Laravel: ```php // Do the slow part first, outside the transaction $data = $api->fetch(); DB::transaction(function () use ($data) { // only fast writes in here Order::create($data); }); ``` ### Raise the timeout (last resort) If a particular job legitimately needs longer, raise the wait timeout rather than leaving it default: ```ini [mysqld] innodb_lock_wait_timeout = 120 ``` But treat this as a stopgap, a growing lock-wait time usually points at transactions that are too long or contention that needs a schema or indexing fix. Related: [MySQL 1040 too many connections](/mysql-1040-too-many-connections) when stuck transactions also exhaust connections. ### SQLSTATE[42S02] Base table or view not found Source: https://rocketeersapp.com/sqlstate-42s02-base-table-or-view-not-found This MySQL error means your query references a table that does not exist in the connected database. In Laravel it almost always means migrations have not run, or you are connected to the wrong database. ## About the error In Laravel the message reads: ```bash SQLSTATE[42S02]: Base table or view not found: 1146 Table 'my_app.users' does not exist ``` The query is valid SQL, but the table it names isn't present in the database you're connected to. ## Why do I see this error - Your migrations haven't run, so the table was never created. - You're connected to the wrong database (wrong `DB_DATABASE`, or a fresh/empty database). - The table name has a typo, or you renamed it without updating the model's `$table`. - A migration failed halfway and the table was rolled back or never committed. ## Solution ### Run your migrations The most common fix, especially on a fresh checkout or a new environment: ```bash php artisan migrate ``` To check what has and hasn't run: ```bash php artisan migrate:status ``` On a development database you can rebuild everything from scratch: ```bash php artisan migrate:fresh ``` Be careful: `migrate:fresh` **drops every table first**. Never run it against a database with data you care about. ### Confirm you're on the right database Verify your connection settings point where you think they do: ```ini DB_DATABASE=my_app ``` Then check the table really exists there: ```sql SHOW TABLES FROM my_app; ``` If the value changed recently, clear any cached config so Laravel reads the new one: ```bash php artisan config:clear ``` ### Check the model's table name If Eloquent guesses the wrong table name, set it explicitly: ```php class Customer extends Model { protected $table = 'customers'; } ``` See [environment variables in Laravel](/environment-variables-laravel) for how the connection is configured. ### SQLSTATE[HY000] [1045] Access denied for user Source: https://rocketeersapp.com/sqlstate-hy000-1045-access-denied-for-user This MySQL error means the username, password or host your application uses to connect is wrong, or the user has no privileges on the database. It is the most common database connection error there is. ## About the error In a Laravel application the full message reads something like: ```bash SQLSTATE[HY000] [1045] Access denied for user 'forge'@'localhost' (using password: YES) ``` `using password: YES` means a password was sent but rejected. `using password: NO` means no password was sent at all, often a sign the value is empty in your config. ## Why do I see this error MySQL refused the login. There are only a handful of reasons: - The username or password is wrong. - The user exists but is not allowed to connect from this host (`'user'@'localhost'` versus `'user'@'%'`). - The user has no privileges granted on the target database. - Your application is reading the wrong credentials, an old `.env`, a cached config, or the wrong environment. ## Solution First, confirm the credentials in your `.env` are correct: ```ini DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=my_app DB_USERNAME=forge DB_PASSWORD=secret ``` Test those exact values directly against MySQL to rule out the application entirely: ```bash mysql -u forge -p -h 127.0.0.1 my_app ``` If that fails too, the problem is in MySQL, not Laravel. Create or fix the user and grant it access: ```sql CREATE USER 'forge'@'localhost' IDENTIFIED BY 'secret'; GRANT ALL PRIVILEGES ON my_app.* TO 'forge'@'localhost'; FLUSH PRIVILEGES; ``` Note the host part. A user created as `'forge'@'localhost'` cannot connect over TCP to `127.0.0.1`, MySQL treats those as different hosts. If your app connects via `127.0.0.1`, grant access to `'forge'@'127.0.0.1'` (or `'%'` for any host). If the direct `mysql` login works but Laravel still fails, your application is reading stale config. Clear it: ```bash php artisan config:clear ``` A cached config (`php artisan config:cache`) freezes whatever values were present at cache time, so re-run it after changing the `.env`. See [environment variables in Laravel](/environment-variables-laravel) for how that loading works. If the password is definitely right but `root` is still rejected, you're likely hitting [1698 access denied (auth_socket)](/mysql-1698-access-denied-auth-socket) instead. ### SQLSTATE[HY000] [2002] Connection refused Source: https://rocketeersapp.com/sqlstate-hy000-2002-connection-refused This error means your application could not even reach the MySQL server. Unlike access denied, the credentials were never checked, the host or port is wrong or the database is not running. ## About the error You'll see one of these two variants: ```bash SQLSTATE[HY000] [2002] Connection refused SQLSTATE[HY000] [2002] No such file or directory ``` This is different from a [1045 Access denied](/sqlstate-hy000-1045-access-denied-for-user) error. There the server rejected your login. Here the server was never reached at all, so it never got as far as checking a username or password. ## Why do I see this error - MySQL isn't running. - `DB_HOST` or `DB_PORT` points to the wrong place. - The classic mix-up: `Connection refused` over TCP versus `No such file or directory` over a socket. The two messages are a useful hint: - **Connection refused** means the app tried to reach MySQL over TCP (a host and port) and nothing was listening there. - **No such file or directory** means the app tried to use a Unix socket and the socket file wasn't where it expected. ## Solution First, make sure MySQL is actually running: ```bash systemctl status mysql systemctl start mysql ``` Then check your host and port. Using `127.0.0.1` forces a TCP connection, while `localhost` makes MySQL use a Unix socket, which is a frequent source of confusion: ```ini DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 ``` Confirm MySQL is listening on that port: ```bash ss -tlnp | grep 3306 ``` Test the connection independently of your app: ```bash mysql -u forge -p -h 127.0.0.1 -P 3306 ``` If you're running MySQL in **Docker**, `127.0.0.1` from inside another container points at the container itself, not the database. Use the service name from your compose file as the host instead: ```ini DB_HOST=mysql ``` After changing anything in `.env`, clear the cached config: ```bash php artisan config:clear ``` ### MySQL ERROR 1698: Access denied for user 'root'@'localhost' Source: https://rocketeersapp.com/mysql-1698-access-denied-auth-socket On a fresh Ubuntu install, logging into MySQL as root fails even with the right password. The cause is the auth_socket plugin, not a wrong password. ## About the error ```bash ERROR 1698 (28000): Access denied for user 'root'@'localhost' ``` You're certain the password is correct, yet `mysql -u root -p` is rejected. This is the signature of the **`auth_socket`** authentication plugin, and it's specifically a 1698, not the [1045 access denied](/sqlstate-hy000-1045-access-denied-for-user) you'd get from genuinely wrong credentials. ## Why do I see this error On modern Ubuntu/Debian, the MySQL `root` account is created to authenticate with the `auth_socket` (or `unix_socket`) plugin instead of a password. That plugin checks the **operating system user** running the client: if the OS user is `root`, it lets you in; otherwise it refuses, regardless of any password. So `mysql -u root -p` as a normal user fails, while `sudo mysql` (running as the OS root) succeeds. ## Solution ### Log in the way the plugin expects The quickest path in is simply: ```bash sudo mysql ``` No password, because `auth_socket` trusts the OS root user. ### Switch root to password authentication If you want classic password login (for a GUI client or a remote tool), change the plugin for the root account. Log in with `sudo mysql`, then: ```sql ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'your-strong-password'; FLUSH PRIVILEGES; ``` On older MySQL/MariaDB use `mysql_native_password` instead of `caching_sha2_password`. ### Better: create a dedicated app user Rather than handing your application the root account, create a least-privilege user scoped to one database: ```sql CREATE USER 'app'@'localhost' IDENTIFIED BY 'your-strong-password'; GRANT ALL PRIVILEGES ON my_app.* TO 'app'@'localhost'; FLUSH PRIVILEGES; ``` Then point your app at `app` rather than `root`. See [environment variables in Laravel](/environment-variables-laravel) for wiring the credentials in. ### MySQL ERROR 1040: Too many connections Source: https://rocketeersapp.com/mysql-1040-too-many-connections This error means MySQL hit its max_connections limit and is refusing new clients. Raise the limit if it is genuinely too low, but first rule out leaked connections. ## About the error ```bash ERROR 1040 (HY000): Too many connections ``` MySQL allows a fixed number of simultaneous client connections (`max_connections`, default 151). Once that ceiling is reached, every new connection is rejected until an existing one closes. For a web app this surfaces as failed requests across the whole site. ## Why do I see this error - `max_connections` is genuinely too low for your traffic. - A **connection leak**: the application opens connections but never closes them. - Long-running queries holding slots open, see `SHOW PROCESSLIST`. - A connection pool misconfigured to exceed the server's limit. - A real traffic spike. ## Solution ### See what's using the connections You'll usually need an account with the `SUPER` privilege (MySQL reserves one extra slot for it) to get in when full: ```sql SHOW PROCESSLIST; SHOW STATUS WHERE Variable_name = 'Threads_connected'; ``` If you see dozens of idle `Sleep` connections, that's a leak or idle sessions not being reaped, not a reason to keep raising the limit. ### Raise the limit Temporarily, without a restart: ```sql SET GLOBAL max_connections = 300; ``` Permanently, in `my.cnf` under `[mysqld]`: ```ini [mysqld] max_connections = 300 ``` Then restart MySQL. Don't set this arbitrarily high, each connection consumes memory, and a too-high value can exhaust RAM and take the server down harder than the original error. ### Reclaim idle connections faster Lower the idle timeouts so abandoned connections free their slots sooner: ```ini wait_timeout = 120 interactive_timeout = 120 ``` ### Fix the real cause in the app For a Laravel app, make sure you're not opening connections in a loop and that long jobs run on a queue rather than holding a web request open. A leak will exhaust any limit you set. Related: [SQLSTATE[HY000] [2002] Connection refused](/sqlstate-hy000-2002-connection-refused) when MySQL can't be reached at all, and [lock wait timeout exceeded (1205)](/mysql-1205-lock-wait-timeout-exceeded) when stuck transactions pile up. ### SQLSTATE[23000] 1062 Duplicate entry Source: https://rocketeersapp.com/sqlstate-23000-1062-duplicate-entry This MySQL error means you tried to insert or update a row with a value that already exists in a unique or primary key column. It is an integrity constraint doing exactly its job. ## About the error In Laravel the message looks like: ```bash SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'jane@example.com' for key 'users.users_email_unique' ``` It tells you three useful things: the value that clashed (`jane@example.com`), the column it clashed on (via the key name), and that it was a uniqueness rule. MySQL refused the write to keep the column unique. ## Why do I see this error - You inserted a record whose unique column (an email, a slug, a username) already exists. - A seeder or migration was run twice. - An auto-increment primary key got out of sync, often after importing data. - A race condition: two requests inserted the "same" new record at almost the same time. ## Solution ### Validate before inserting In Laravel, catch duplicates with a validation rule so the user gets a clean message instead of a 500: ```php $request->validate([ 'email' => 'required|email|unique:users,email', ]); ``` ### Use upsert / updateOrCreate for idempotent writes If re-running the operation should update rather than fail, don't blind-insert: ```php User::updateOrCreate( ['email' => $email], // match on this ['name' => $name], // update these ); ``` For bulk operations, `upsert()` does the same in a single query. ### Reset a broken auto-increment If the clash is on the primary key after an import, realign the counter to just past the highest existing id: ```sql ALTER TABLE users AUTO_INCREMENT = 1; ``` MySQL bumps the value to the next free id automatically. ### Re-running seeders If a seeder keeps colliding, either make it idempotent with `updateOrCreate`, or refresh the schema first on a development database: ```bash php artisan migrate:fresh --seed ``` Remember `migrate:fresh` drops all tables, so never run it where there's data you need. For other migration-time failures, see [1064 SQL syntax error](/mysql-1064-sql-syntax-error) and [42S02 base table or view not found](/sqlstate-42s02-base-table-or-view-not-found). ### MySQL max_allowed_packet: packet too large Source: https://rocketeersapp.com/mysql-max-allowed-packet-packet-too-large This error means a single query or row exceeded the maximum packet size MySQL accepts. It shows up most often when importing a large dump or storing big blobs. ## About the error You'll hit one of these: ```bash ERROR 1153 (08S01): Got a packet bigger than 'max_allowed_packet' bytes ERROR 2006 (HY000): MySQL server has gone away ``` When a client or the server receives a single packet larger than `max_allowed_packet`, it rejects it and drops the connection, which often surfaces as the misleading "server has gone away" message. ## Why do I see this error - Importing a database dump that contains very large multi-row `INSERT` statements. - Storing large `BLOB` / `TEXT` values, like images or big JSON columns. - A bulk insert that builds one enormous query. ## Solution ### Raise the limit for the running server You can set it at runtime without a restart (the value is in bytes, this is 256 MB): ```sql SET GLOBAL max_allowed_packet = 268435456; ``` A runtime change is reset on restart, so make it permanent in `my.cnf` (`/etc/mysql/my.cnf` or a file under `/etc/mysql/conf.d/`): ```ini [mysqld] max_allowed_packet = 256M ``` Then restart MySQL: ```bash systemctl restart mysql ``` ### Set it for an import When the error happens during an import, the **client** also has its own limit. Pass it on the command line so both sides agree: ```bash mysql --max_allowed_packet=256M -u forge -p my_app < dump.sql ``` See [importing a database from the command line](/import-database-mysql-command-line) for the full import workflow. ### Check the current value To see where it's set right now: ```sql SHOW VARIABLES LIKE 'max_allowed_packet'; ``` If you regularly export and re-import large databases, it's worth setting a generous value on both the source and destination so dumps move cleanly. See [exporting a database from the command line](/export-database-mysql-command-line). A related connection-drop error is [MySQL server has gone away (2006)](/mysql-2006-server-has-gone-away), which an oversized packet can trigger. ### MySQL ERROR 2006: server has gone away Source: https://rocketeersapp.com/mysql-2006-server-has-gone-away This error means the connection to MySQL was lost mid-query. The usual causes are an idle timeout, a query larger than max_allowed_packet, or the server restarting. ## About the error ```bash ERROR 2006 (HY000): MySQL server has gone away ``` It means the client sent a query but the connection to the server was already closed or dropped. The message is generic, MySQL is telling you the link is gone, not why. ## Why do I see this error Three causes account for almost every case: - **Idle timeout**: the connection sat idle longer than `wait_timeout` and MySQL closed it. Common with long-running queue workers and scheduled jobs. - **Oversized query**: a single statement exceeded `max_allowed_packet`, so the server dropped the connection. See [MySQL max_allowed_packet: packet too large](/mysql-max-allowed-packet-packet-too-large). - **The server restarted or was OOM-killed** while the client held a connection. ## Solution ### Idle timeout If the error happens after periods of inactivity, raise the idle timeout in `my.cnf`: ```ini [mysqld] wait_timeout = 600 interactive_timeout = 600 ``` Then restart MySQL. For a long-running PHP worker, the more robust fix is to **reconnect** rather than hold one connection open for hours. In Laravel, a queue worker that reconnects on each job avoids the problem; you can also have the worker restart periodically: ```bash php artisan queue:work --max-time=3600 ``` ### Oversized query If it happens on a specific large insert or import, the packet limit is the cause, not the timeout. Raise it on both server and client: ```ini [mysqld] max_allowed_packet = 256M ``` See the dedicated [max_allowed_packet article](/mysql-max-allowed-packet-packet-too-large) for the full walkthrough. ### Server restarted If neither applies, check whether MySQL crashed or was killed. Look at the MySQL error log and the system journal: ```bash sudo tail -n 50 /var/log/mysql/error.log sudo journalctl -u mysql --since "1 hour ago" ``` An out-of-memory kill points at server sizing, see [adding swap space on Ubuntu](/add-swap-space-on-ubuntu). ### MySQL ERROR 1064: SQL syntax error Source: https://rocketeersapp.com/mysql-1064-sql-syntax-error Error 1064 means MySQL could not parse your query. The message points at exactly where parsing failed, the cause is usually a reserved word, a typo, or a quoting problem. ## About the error ```bash ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '...' at line 1 ``` The crucial part is the text after **`near`**. MySQL prints the query starting from the point where it got confused, so the problem is almost always at or just *before* that snippet, not somewhere else. ## Why do I see this error - A **reserved word** used as a table or column name without backticks (`order`, `group`, `rank`, `key`, `desc`). - A typo in a keyword (`SELCT`, `FORM`, `WHERE` placed wrong). - A missing or mismatched quote or parenthesis. - A stray trailing comma before `FROM` or a closing bracket. - Mixing in another SQL dialect's syntax that MySQL doesn't accept, often when [importing a dump](/import-database-mysql-command-line) made on a different server or version. ## Solution ### Read the "near" snippet If you see `near 'order (id int...'`, the word right after `near` (`order`) is the offender. `ORDER` is reserved, so quote it with backticks: ```sql CREATE TABLE `order` ( `id` INT PRIMARY KEY, `rank` INT ); ``` Backticks let you use reserved words as identifiers, though renaming the column to something non-reserved is cleaner long term. ### Check quoting Use single quotes for string values and backticks for identifiers, never the other way around: ```sql -- wrong SELECT * FROM users WHERE name = "O'Brien"; -- right SELECT * FROM users WHERE name = 'O\'Brien'; ``` ### When it comes from application code If the query is built by your app, the syntax error usually means a variable was interpolated into the SQL unescaped, which is also a SQL injection risk. Use parameter binding instead of string concatenation. In Laravel, prefer the query builder or bindings: ```php DB::select('select * from users where email = ?', [$email]); ``` This both fixes the syntax problem and closes the injection hole. If MySQL parses the query but can't find the table, that's a different error: [base table or view not found](/sqlstate-42s02-base-table-or-view-not-found). And once the query runs, the next question is usually whether it runs fast, see [finding and optimizing slow MySQL queries](/mysql-query-optimization). ### How to rename a MySQL database Source: https://rocketeersapp.com/rename-mysql-database Unlike renaming a table, you can't rename a database in MySQL sadly enough. So there is no SQL query you can execute to achieve this. However, it is possible with a workaround. The most important step: To get going with this, first [create a full backup of your MySQL database](/backup-mysql-databases-single-file) to make sure you don't loose any data! ## Create a newly named database This step involves creating the database you want to rename the old database to: ```sql CREATE DATABASE `NEW_DATABASE_NAME`; ``` ## Dump & import Dump and import the old database into your new database: ```bash mysqldump -u USERNAME -p OLD_DATABASE_NAME | mysql -u USERNAME -p NEW_DATABASE_NAME ``` ## Drop the old database If your really sure, you can (with the backup on hand) remove the old database safely: ```sql DROP DATABASE `OLD_DATABASE_NAME` ``` ### Import MySQL database without timezone difference Source: https://rocketeersapp.com/mysql-import-without-timezone-difference When you import a MySQL database from a server to your local computer, you might notice that the dates and times are different. ## Why is that? This is because there is a timezone difference between your computer (probably in your local time) and the server (probably in UTC as it should), while your timestamp columns don't have a timezone specified. As it is your personal computer, you want the time to display in your local timezone. But when developing, you should want everything to be in `UTC` timezone to prevent al sorts of problems (like this one!) and to easily convert and display the correct time(zone) to your users. ## MySQL timezone setting When you this query in MySQL: ```sql SELECT @@global.time_zone; ``` You probably get the default value `SYSTEM` returned, this means MySQL uses the timezone your computer is set to. ## How can you fix this? Before import a database, make sure to change the timezone to `UTC` by running: ```bash sudo mysql -e "SET GLOBAL time_zone = '+0:00';" ``` While this fixes it for your import, this value is being reset when you restart the MySQL instance. To make this change permanent, change it in `/etc/mysql/my.cnf`: ```bash sudo nano /etc/mysql/my.cnf # linux sudo nano /opt/homebrew/etc/my.cnf # macos ``` Add or change under the section `[mysqld]` the value for `default-time-zone`: ```bash default-time-zone = "+0:00" ``` To make the change active, restart MySQL: ```bash sudo service mysql restart # linux brew services restart mysql # macos ``` ### How to install PostgreSQL with pgvector on Ubuntu Source: https://rocketeersapp.com/install-postgresql-pgvector-ubuntu Turn PostgreSQL in a vector database using the pgvector extension on Ubuntu 22. We assume you already have provisioned a Ubuntu server, ready to get going. ## Install PostgreSQL If you haven't already installed PostgreSQL on your server, execute these commands to install it from the official repository: ```bash sudo apt install -y postgresql-common sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh ``` ## Install pgvector When PostgreSQL is installed, run the following command: ```bash sudo apt install postgresql-17-pgvector ``` ## Enable the pgvector extension The last step to turn PostgreSQL in a vector database, run the following query as the postgres user: ```bash sudo -u postgres psql # run as postgres user ``` ```sql CREATE EXTENSION vector; # create the vector extension ``` ## Start PostgreSQL automatically (optional) Configure PostgreSQL to start automatically after booting your server: ```bash sudo systemctl enable postgresql sudo service postgresql start ``` ### Backup MySQL databases except system databases in a single file Source: https://rocketeersapp.com/backup-mysql-databases-except-system How to dump all MySQL databases on a server in a single file with the exception of certain (system) databases. ## MySQL backup bash script A clear and simple bash script to export all MySQL databases on your server into a single backup file, excluding databases you don't want to backup. With inline comments, this is the bash script to export all databases into a single file with exception of certain databases: ```bash # MySQL username & password USER="" PASSWORD="" # MySQL dump options OPTIONS="--add-drop-table --extended-insert --single-transaction --skip-comments" # Skip exporting these databases, separated by "|" SKIP="Database|mysql|sys|information_schema|performance_schema" # Get all databases on server, except the skipped ones DATABASES=` echo "SHOW DATABASES;" | mysql --user="$USER" --password="$PASSWORD" | grep -v -E "^(${SKIP})$" | tr '\n' ' ' ` # Export database into a single file mysqldump \ --user="$USER" \ --password="$PASSWORD" \ $OPTIONS \ --databases $DATABASES \ > ./databases.sql ``` ## How to use You can use this by copying the script, save it in an `backup.sh` file and then execute `chmod +x ./backup.sh` to give the file execution permissions. Execute the script with `./backup.sh`. ## Related Prefer a different layout? You can also [backup all databases into a single file](/backup-mysql-databases-single-file) or [backup each database into separate files](/backup-mysql-databases-separate-files). To keep backups off the server itself, [stream the backup directly to an S3 bucket](/stream-mysql-backup-s3-bucket). ## Let Rocketeers handle it Rolling your own backup scripts works, but they're easy to forget and easy to break. Rocketeers can [safely back up your files and databases](/features/safely-backup-files-and-databases) on a schedule, streamed straight to cloud storage. ### Backup MySQL databases in single file Source: https://rocketeersapp.com/backup-mysql-databases-single-file How to dump existing MySQL databases on a server in a single file. ## MySQL backup bash script A clear and simple bash script to export all MySQL databases on your server into separate `.SQL` files. With inline comments, this is the bash script to export all databases into separate files: ```bash # MySQL username & password USER="" PASSWORD="" # MySQL dump options OPTIONS="--add-drop-table --extended-insert --single-transaction --skip-comments" mysqldump --user="$USER" --password="$PASSWORD" $OPTIONS --all-databases > ./databases.sql ``` ## How to use You can use this by copying the script, save it in an `backup.sh` file and then execute `chmod +x ./backup.sh` to give the file execution permissions. Execute the script with `./backup.sh`. Prefer a different layout? Back up [each database to a separate file](/backup-mysql-databases-separate-files), [exclude the system databases](/backup-mysql-databases-except-system), or [stream straight to an S3 bucket](/stream-mysql-backup-s3-bucket). ### How to upgrade MySQL 5.7 to 8.0 on Ubuntu Source: https://rocketeersapp.com/upgrade-mysql-5-7-to-8-0-ubuntu Learn how to safely upgrade your database server from MySQL 5.7 to MySQL 8.0 on Ubuntu. ### Backup existing databases (don't skip) Don't skip this part, it can save you a lot of trouble when something goes wrong! [Backup the databases in separate files](/backup-mysql-databases-separate-files) or choose to [backup your databases in a single file](/backup-mysql-databases-single-file). Possibly the latter is easier when needing to import the backup and importing it all at once. ### Remove current MySQL version ```bash sudo apt purge mysql-* ``` ### Install essential packages and add apt key ```bash sudo apt install -y dirmngr # essential package for adding apt key sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29 ``` ### Add MySQL 8.0 repository source ```bash echo "deb http://repo.mysql.com/apt/ubuntu $(lsb_release -sc) mysql-8.0" | sudo tee /etc/apt/sources.list.d/mysql-8.0.list ``` ### Update apt repositories & install MySQL 8.0 ```bash sudo apt-get update sudo apt-get install -y mysql-server mysql-client ``` ### Check if MySQL 8.0 is installed When everything goes well, you're done. MySQL version 8.0 is installed. You can verify this by checking the status of the MySQL daemon and the MySQL version: ``` sudo service mysql status ``` This should output that MySQL is `active (running)`. To make sure you're on the new version, check the MySQL version: ```bash sudo mysql --version ``` Should show something like: ``` mysql Ver 8.0.31 for Linux on x86_64 (MySQL Community Server - GPL) ``` ### Help! MySQL doesn't start anymore Check the error logs of MySQL to find the cause, most of the times it provides a clear message and it's probably because of some deprecated configuration inside in one of the `/etc/mysql` config files. An example of an error message I have got in the past. Removing `NO_AUTO_CREATE_USER` from the `sql_mode` fixed the issue: ``` 2023-01-12T09:21:12.834467Z 0 [ERROR] [MY-000077] [Server] /usr/sbin/mysqld: Error while setting value 'NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' to 'sql_mode'. ``` ### Backup MySQL databases in separate files Source: https://rocketeersapp.com/backup-mysql-databases-separate-files How to dump existing MySQL databases on a server in separate backup files. ## MySQL backup bash script A clear and simple bash script to export all MySQL databases on your server into separate `.SQL` files. With inline comments, this is the bash script to export all databases into separate files: ```bash # MySQL username & password USER="" PASSWORD="" # MySQL dump options OPTIONS="--add-drop-table --extended-insert --single-transaction --skip-comments" # Skip exporting these databases, separated by "|" SKIP="Database|mysql|sys|information_schema|performance_schema" # Get all databases on server DATABASES=` echo "SHOW DATABASES;" | mysql --user="$USER" --password="$PASSWORD" | grep -v -E "^(${SKIP})$" ` # Loop through databases for DATABASE in $DATABASES do # Export database into separate file mysqldump \ --user="$USER" \ --password="$PASSWORD" \ $OPTIONS \ "$DATABASE" \ > ./$DATABASE.sql done ``` ## How to use You can use this by copying the script, save it in an `backup.sh` file and then execute `chmod +x ./backup.sh` to give the file execution permissions. Execute the script with `./backup.sh`. Prefer a different layout? Back up [everything into a single file](/backup-mysql-databases-single-file), [exclude the system databases](/backup-mysql-databases-except-system), or [stream straight to an S3 bucket](/stream-mysql-backup-s3-bucket). ### Export MySQL database using command line Source: https://rocketeersapp.com/export-database-mysql-command-line Exporting a MySQL database is a task you will need very often when working with databases. The best option to export a MySQL database as quick as possible is using the command line. You can even get fancy using compression, additional parameters to customize the export format and stream directly to an external location to save it somewhere else than your local disk. ## MySQL dump command In the most minimal way, you can dump a MySQL database using `mysqldump` and the `--add-drop-table` parameter. Using this parameter makes it more easy to importing the file on an existing database. Without it, you will get errors when the table already exists on the destination database. ```bash mysqldump --user=[username] --password=[password] --add-drop-table [database] ``` ## Compress the backup file When exporting the database to a backup file, it is most likely you want to compress the file to a smaller file size when saving or distributing it to another destination. To compress the file, we can use good old `gzip`. The `-9` parameter can be lowered to `-1` make the export go faster, but the exported file larger. ```bash mysqldump --user=[username] --password=[password] --add-drop-table [database] | gzip -9 ``` ## Optimize the exported file To optimize the exported file a little bit further, we can use several parameters that make the import process smoother and remove some fluf that's not needed in the final export. In the order of the command below, we: - Drop tables before creating them (`--add-drop-table`) - Use table locking to improve import speed (`--add-locks`) - Remove unneeded column statistics (`--column-statistics=0`) - Insert all data using one insert statement per table (`--extended-insert`) - Prevent acces denied errors by not using tablespaces (`--no-tablespaces`) - Within a single transaction to prevent an inconsistent state of the database before importing (`--single-transaction`) - Remove comments by skipping them in the output (`--skip-comments`) - Define the charset used in the export (`--set-charset`) - Read large tables without needing enough RAM to fit the full table in memory (`--quick`) ```bash mysqldump --user=[username] --password=[password] --add-drop-table --add-locks --column-statistics=0 --extended-insert --no-tablespaces --single-transaction --skip-comments --set-charset --quick [database] | gzip -9 ``` ## Dump MySQL database to external storage When creating backups it's desirable to save it somewhere else, because a backup file on the same disk as the database can be a single point of failure. In case your storage disk is damaged, the database and backup file are gone. [Read how to stream MySQL back-up directly to S3 bucket](/stream-mysql-backup-s3-bucket) ### Importing database in MySQL using command line Source: https://rocketeersapp.com/import-database-mysql-command-line Importing a database in MySQL is quick & easy using the command line. ## Different options Importing a MySQL database can be done in several ways, using a desktop app or even using the legendary phpMyAdmin! But the most effective and fast way to import a (large) SQL file remains - as always - using the command line interface (CLI). ## Using the command line Using the command below you can import large SQL files in the least amount of time: ```bash mysql -u [username] -p [database] < import.sql ``` Replace `[username]`, `[database]` and `import.sql` with the correct values in your setup. To go the other way, see [exporting a database](/export-database-mysql-command-line). If timestamps shift on import, fix the [timezone difference](/mysql-import-without-timezone-difference). And to rename a database afterwards, see [rename a MySQL database](/rename-mysql-database). ### Stream MySQL backup directly to S3 bucket Source: https://rocketeersapp.com/stream-mysql-backup-s3-bucket Backing up your database is very important. Creating a backup in the most efficient way is essential when you do not have unlimited disk space. ## Offsite backup location When creating backups for your database it is recommended to save the backups to an offsite storage facility. This way when you crash or delete the wrong files on your server, you'll always have the backups in place on another filesystem. And if this is the case, then you'll probably want to know how to stream the backup directly to the external location! Because saving the file locally first would be very inefficient and also consumes a lot of diskspace. So a simple `mysqldump` to save to local disk won't do it anymore. Let's have a look! ## How to stream the backup directly to S3 First we start the backup using `mysqldump`, I have some preferred parameters on there, but you can change this to your own preferences. Then we pipe the data directly to `gzip` to compress the data first, we use the highest (`9`) compression to keep storage costs low as possible. After compressing we pipe it through `pv` to make sure we don't hit any bandwidth limits, we use here a limit of 1MB (`1m`) per second. In case you want faster uploads you should increase this. After that we can safely stream the data to S3 using the `s3cmd` command line tool, which can be installed very easily using `apt`. We use the `--acl-private` option to make sure the backup can never be accessed publicly. If you haven't set up the AWS tooling yet, see [getting started with the AWS CLI](/getting-started-aws-cli); Rocketeers can also stream backups to [AWS](/providers/aws) or [Backblaze](/providers/backblaze) for you. ```bash # Credentials DATABASE="Your database name here" PASSWORD="Your database password here" BACKUP_PATH="File path for saving on the S3 bucket here" # Backup command mysqldump --password=$PASSWORD --add-drop-table --column-statistics=0 --extended-insert --no-tablespaces --single-transaction --skip-comments $DATABASE | gzip -9 | pv -L 1m -q | s3cmd --acl-private put - s3://$BACKUP_PATH; ``` ## Development ### How to install Node.js on Ubuntu Source: https://rocketeersapp.com/how-to-install-nodejs Modern PHP sites still need Node.js to build their frontend assets. Here is how to install Node.js on Ubuntu with nvm, so you can switch versions per project without touching the system. Even a PHP application usually needs Node.js — to compile CSS, bundle JavaScript with Vite or webpack, and run the asset build step during [deployment](/zero-downtime-php-deployments). The cleanest way to install it is [nvm](https://github.com/nvm-sh/nvm), the Node Version Manager, which keeps Node in your home directory so different projects can run different versions without `sudo`. ## Install nvm Run the official install script (pin it to a specific nvm version so the install is reproducible): ```bash curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash ``` The script appends the loader to your shell profile. Either open a new shell or load it into the current one: ```bash export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" ``` Confirm it's available: ```bash nvm --version ``` ## Install Node.js Install the version you want and set it as the default for new shells: ```bash nvm install 22 nvm alias default 22 ``` Check both Node and npm came along: ```bash node -v npm -v ``` ## Switch versions per project The whole point of nvm is that one server can run several Node versions. Install another and switch with a single command: ```bash nvm install 20 nvm use 20 ``` Drop a `.nvmrc` file containing a version number (for example `22`) in a project's root and `nvm use` will pick it up automatically — handy for keeping a deploy script on the right version. ## Building assets on deploy In a deployment you'll typically install dependencies and run a build: ```bash npm ci npm run build ``` Use `npm ci` rather than `npm install` on a server — it's faster and installs exactly what's in the lockfile. The difference matters more than it looks; see [npm ci vs npm install](/npm-ci-vs-npm-install). ## The alternative: NodeSource If you'd rather install Node system-wide (every user, no per-shell loading), the NodeSource apt repository is the common alternative. nvm wins when you need multiple versions or want to avoid `sudo`; NodeSource wins for a single fixed version on a dedicated box. ## Let Rocketeers handle it Installing nvm, loading it in the right shell profile, pinning versions per project, and making sure the deploy step runs with the correct Node version is one more moving part to keep aligned. Rocketeers installs Node.js during provisioning and runs your asset build as part of [every deployment](/features/zero-downtime-atomic-deployments) — so the frontend is compiled and live without a separate setup. ### Fix: Cannot connect to the Docker daemon at unix:///var/run/docker.sock Source: https://rocketeersapp.com/cannot-connect-to-docker-daemon This error means your Docker client could not reach the Docker daemon. Either the daemon is not running, or your user does not have permission to talk to its socket. ## About the error ```bash Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? ``` The `docker` command you type is just a client. It sends instructions to a background service, the Docker daemon (`dockerd`), over a Unix socket at `/var/run/docker.sock`. This error appears when the client cannot reach that daemon. ## Why do I see this error - The Docker daemon is not running. - Your user is not allowed to access the socket (a permissions issue). - Your client is pointed at the wrong Docker context or a stale `DOCKER_HOST`. ## Solution: start the daemon On **macOS or Windows**, the daemon runs inside Docker Desktop. Make sure Docker Desktop is open and has finished starting (the whale icon stops animating). On **Linux**, start and enable the service so it also comes back after a reboot: ```bash sudo systemctl start docker sudo systemctl enable docker ``` Check that it is actually running: ```bash sudo systemctl status docker ``` ## Solution: fix the permission denied case If the daemon is running but you still see the error (often phrased as `permission denied while trying to connect`), your user is not in the `docker` group. Add it: ```bash sudo usermod -aG docker $USER ``` Group membership only applies to new sessions, so log out and back in, or start a fresh shell with: ```bash newgrp docker ``` Then verify: ```bash docker run hello-world ``` ## Still failing? Check the context If Docker is running and you have permission, your client may be pointed somewhere else. List your contexts and confirm the active one: ```bash docker context ls ``` Also check for a leftover environment variable overriding the socket: ```bash echo $DOCKER_HOST ``` If `DOCKER_HOST` is set to something unexpected, unset it and try again. Once the daemon is reachable, you can [clean up unused Docker data with prune](/docker-prune) or [run a command in a container with docker exec](/docker-exec). ### docker compose up: options and common flags Source: https://rocketeersapp.com/docker-compose-up docker compose up reads your compose file and starts every service in it. The flags you reach for most are -d to run in the background and --build to rebuild images first. ## Start your services Run from the directory containing your `compose.yaml` (or `docker-compose.yml`): ```bash docker compose up ``` This creates and starts every service defined in the file, and streams their logs to your terminal. Press `Ctrl+C` to stop them. ## Run in the background Most of the time you want the stack running detached, so your terminal is free: ```bash docker compose up -d ``` Check what is running and follow the logs separately: ```bash docker compose ps docker compose logs -f ``` ## Rebuild images first If you changed a `Dockerfile` or the build context, force a rebuild before starting: ```bash docker compose up -d --build ``` To throw away cached containers and recreate them from scratch: ```bash docker compose up -d --force-recreate ``` ## Start only some services Pass service names to start a subset. Add `--no-deps` if you do not want its linked services started too: ```bash docker compose up -d web docker compose up -d --no-deps web ``` ## Stopping again `Ctrl+C` stops a foreground `up`. For a detached stack, bring it down explicitly. `down` also removes the containers and network; `stop` just halts them: ```bash docker compose down # stop and remove containers + network docker compose stop # stop but keep the containers ``` ## A note on the command name Modern Docker uses `docker compose` (a built-in subcommand, Compose v2). The older standalone tool was `docker-compose` with a hyphen. If `docker compose` is not found, you are likely on an old install, upgrade Docker, or fall back to `docker-compose`. Once it is up, [docker exec](/docker-exec) lets you run commands inside a running service, and [docker prune](/docker-prune) cleans up afterwards. If `docker compose` can't reach the engine at all, see [cannot connect to the Docker daemon](/cannot-connect-to-docker-daemon). ### How to checkout a Git tag Source: https://rocketeersapp.com/git-checkout-tag Checking out a tag puts you in a detached HEAD state, which is fine for inspecting a release but not for committing. If you want to make changes, create a branch from the tag. ## Checkout a tag A tag points at a specific commit, usually a release. Check it out by name: ```bash git checkout v1.2.0 ``` To be unambiguous (in case a branch and a tag share a name), use the full ref: ```bash git checkout tags/v1.2.0 ``` ## You are now in detached HEAD After checking out a tag, Git warns you that you are in a **detached HEAD** state. That means `HEAD` points directly at a commit instead of a branch. You can look around, build, and run the code, but any commits you make here are not on a branch and will be lost once you switch away. ## Make changes from a tag: create a branch If you need to work from a tagged release, for example to patch an old version, create a branch from the tag: ```bash git checkout -b hotfix-1.2.0 v1.2.0 ``` The modern equivalent uses `git switch`: ```bash git switch --detach v1.2.0 # just inspect git switch -c hotfix-1.2.0 v1.2.0 # branch off the tag ``` ## The tag is missing? If `git checkout v1.2.0` fails with `pathspec ... did not match`, your clone probably does not have the tag yet. Fetch tags from the remote: ```bash git fetch --tags ``` List the tags you have to confirm the exact name: ```bash git tag ``` ### How to remove untracked files in Git (git clean) Source: https://rocketeersapp.com/git-remove-untracked-files git clean deletes untracked files from your working directory. Always dry-run it first with -n, because unlike most Git operations these deletions cannot be undone. ## Preview first with a dry run `git clean` permanently deletes files that Git is not tracking. Untracked files were never committed, so there is no history to recover them from. **Always preview** with `-n` (dry run) before deleting anything: ```bash git clean -n ``` This lists what *would* be removed without touching anything. ## Remove untracked files When you are happy with the preview, `-f` (force) actually deletes them. Git requires the flag as a safety measure: ```bash git clean -f ``` To also remove untracked **directories**, add `-d`: ```bash git clean -fd ``` ## Including ignored files By default `git clean` leaves files listed in `.gitignore` alone (your `node_modules`, build output, `.env`, and so on). To remove ignored files as well, add `-x`: ```bash git clean -fdx ``` To remove **only** ignored files and keep other untracked ones, use `-X` (uppercase): ```bash git clean -fdX ``` ## Interactive mode If you want to choose file by file, use interactive mode: ```bash git clean -i ``` ## clean vs reset These two solve different problems: - `git clean` removes **untracked** files (files Git has never recorded). - [git reset --hard](/git-reset-hard) discards changes to **tracked** files. To stop tracking a file that was committed by mistake but keep it on disk, that is a different task again, see [Removing tracked files in Git that should have been ignored](/removing-tracked-files-git). ### docker exec: run a command in a running container Source: https://rocketeersapp.com/docker-exec docker exec runs a command inside a container that is already running. The classic use is opening an interactive shell with docker exec -it, but it is just as useful for one-off commands. ## Open a shell inside a container The most common use of `docker exec` is getting a shell in a running container. The `-it` flags give you an interactive terminal: ```bash docker exec -it my-container bash ``` If the image is minimal and has no `bash`, fall back to `sh`: ```bash docker exec -it my-container sh ``` The `-i` keeps STDIN open and `-t` allocates a pseudo-TTY; together they make the shell behave like a normal terminal. ## Run a one-off command You do not need a shell for a single command. Anything after the container name is run inside it: ```bash docker exec my-container ls -la /app docker exec my-container cat /etc/hostname ``` For a Laravel container you might run Artisan, or open a database client: ```bash docker exec -it my-app php artisan migrate docker exec -it my-db mysql -u root -p ``` ## Useful flags ```bash docker exec -u www-data my-container whoami # run as a specific user docker exec -w /app my-container pwd # set the working directory docker exec -e DEBUG=1 my-container env # pass an environment variable ``` ## exec vs run vs attach These look similar but do different things: - `docker exec` runs a command in a container that is **already running**. - `docker run` creates a **new** container from an image. - `docker attach` connects to the container's main process (its PID 1), so `Ctrl+C` there can stop the container, which is usually not what you want. To find the container name or ID to pass to `exec`, list running containers with `docker ps`. If you started everything with Compose, see [docker compose up](/docker-compose-up). ### How to clean up Docker with prune (images, volumes, system) Source: https://rocketeersapp.com/docker-prune Docker hangs on to stopped containers, unused images and dangling volumes, which quietly eat disk space. The prune commands clean them up, but a couple of flags decide how aggressive that cleanup is. ## See what Docker is using Before deleting anything, check where the space has gone: ```bash docker system df ``` This breaks down disk usage by images, containers, local volumes and build cache. ## Prune everything unused at once `docker system prune` removes stopped containers, unused networks, dangling images and the build cache in one go: ```bash docker system prune ``` It asks for confirmation and, by default, only removes **dangling** images (layers no longer referenced by any tag). To also remove every image not used by a running container, add `-a`: ```bash docker system prune -a ``` Volumes are **not** touched unless you ask, because they usually hold data you care about. To include them: ```bash docker system prune -a --volumes ``` Be careful with that last one: it deletes any volume not attached to a running container. ## Prune one resource type For finer control, prune a single category: ```bash docker container prune # remove all stopped containers docker image prune # remove dangling images docker image prune -a # remove all unused images docker volume prune # remove unused volumes docker network prune # remove unused networks docker builder prune # clear the build cache ``` ## Skip the confirmation prompt In scripts or cron jobs, add `-f` to skip the "Are you sure?" prompt: ```bash docker system prune -af ``` If you reached for prune because a server filled up, the disk pressure may be coming from more than Docker, see [No space left on device on Ubuntu](/no-space-left-on-device-ubuntu). ### How to delete a local (and remote) Git branch Source: https://rocketeersapp.com/git-delete-local-branch Deleting a branch locally uses git branch -d, with a capital -D to force it. Deleting it on the remote is a separate command that people often forget. ## Delete a local branch Use `-d` (lowercase) to delete a branch that has already been merged. Git refuses if the branch has unmerged commits, which protects you from losing work: ```bash git branch -d feature-login ``` If you are sure you want to discard the branch even though it is not merged, force it with `-D`: ```bash git branch -D feature-login ``` You cannot delete the branch you are currently on. Switch away first: ```bash git switch main git branch -d feature-login ``` ## Delete the remote branch Removing the branch locally does not touch the remote. Delete it there explicitly: ```bash git push origin --delete feature-login ``` An older shorthand does the same thing (note the leading colon): ```bash git push origin :feature-login ``` ## Clean up stale remote-tracking branches After a branch is deleted on the remote, your local clone may still show it under `git branch -r`. Prune those references: ```bash git fetch --prune ``` To rename a branch instead of deleting it, see [How to rename a Git branch](/git-rename-branch). To check out a tagged release without deleting anything, see [how to check out a Git tag](/git-checkout-tag). ### npm ci vs npm install: when to use which Source: https://rocketeersapp.com/npm-ci-vs-npm-install npm install resolves dependencies and updates your lockfile as it goes. npm ci does a clean, exact install straight from the lockfile, which is what you want in CI and production builds. ## What npm install does `npm install` is the everyday command for adding and updating dependencies: ```bash npm install npm install lodash # add a package ``` It reads `package.json`, resolves versions, installs them, and **writes** the result to `package-lock.json`. (npm ships with Node itself, see [how to install Node.js](/how-to-install-nodejs) if it's missing on a server.) If a dependency allows a newer version within its range, the lockfile can change. That flexibility is exactly what you want while developing. ## What npm ci does `npm ci` (clean install) is built for automated and reproducible installs: ```bash npm ci ``` It behaves differently in three important ways: - It installs **exactly** what is in `package-lock.json`, ignoring version ranges in `package.json`. - It **deletes** `node_modules` first, so you always start from a clean slate. - It **never writes** to `package.json` or `package-lock.json`. It is also typically faster than `npm install` because it skips dependency resolution. ## The catch: the lockfile must exist and match `npm ci` requires a `package-lock.json` (or `npm-shrinkwrap.json`), and that lockfile must be in sync with `package.json`. If they disagree, it fails on purpose rather than silently "fixing" things: ```text npm ci can only install packages when your package.json and package-lock.json are in sync. ... ``` The fix is to run `npm install` locally, commit the updated lockfile, and try again. ## When to use which | Situation | Use | |---|---| | Day-to-day development | `npm install` | | Adding or upgrading a package | `npm install ` | | CI / CD pipelines | `npm ci` | | Production / Docker builds | `npm ci` | The rule of thumb: if you want reproducible, lockfile-exact installs, use `npm ci`. If you intend to change your dependencies, use `npm install`. Because `npm ci` wipes `node_modules`, it also clears up the kind of half-installed state that otherwise has people deleting the folder by hand. The same logic applies to deploy scripts: run `npm ci && npm run build` as part of a [zero-downtime deployment](/zero-downtime-php-deployments), so every release builds from the exact locked versions. If a Laravel app then complains about missing assets, see [Vite manifest not found](/vite-manifest-not-found-laravel). ### git reset --hard explained (soft vs mixed vs hard) Source: https://rocketeersapp.com/git-reset-hard git reset moves your branch to another commit. The --soft, --mixed and --hard flags decide what happens to your staged and working changes, and --hard is the one that actually throws work away. ## What git reset does `git reset` moves the current branch tip to a commit you choose. The flag controls how far the reset reaches: - `--soft`: move the branch, **keep** everything staged. - `--mixed` (the default): move the branch, unstage changes, keep them in your working directory. - `--hard`: move the branch and **discard** all staged and working-directory changes. ## Undo the last commit, keep the work To undo your most recent commit but keep the changes so you can re-commit them, use `--soft` or the default `--mixed`: ```bash git reset --soft HEAD~1 # commit undone, changes still staged git reset HEAD~1 # commit undone, changes unstaged but present ``` ## Discard everything with --hard `--hard` resets the branch and wipes uncommitted changes. Use it when you genuinely want to throw work away: ```bash git reset --hard HEAD # discard all uncommitted changes git reset --hard HEAD~1 # delete the last commit and its changes ``` A common use is forcing your local branch to match the remote exactly: ```bash git fetch origin git reset --hard origin/main ``` This is destructive. Anything not committed is gone. If you only want to clean up *untracked* files (not reset tracked ones), use [git clean](/git-remove-untracked-files) instead. ## Recovering after a --hard reset If you reset away a commit you actually needed, it is usually still findable for a while. `git reflog` records where `HEAD` has been: ```bash git reflog git reset --hard ``` Note that `reflog` only recovers committed work. Changes that were never committed and got discarded by `--hard` cannot be recovered. When in doubt, [stash](/git-stash-pop) your changes instead of resetting. ### git stash pop vs apply: save and restore changes Source: https://rocketeersapp.com/git-stash-pop git stash shelves your uncommitted changes so you can switch context. git stash pop restores them and removes the stash; git stash apply restores them but keeps the stash around. ## Stashing your changes `git stash` takes your modified tracked files, saves them away, and gives you a clean working directory: ```bash git stash ``` By default it does **not** include untracked files. Add `-u` to stash those too: ```bash git stash -u ``` Give a stash a label so you can recognise it later: ```bash git stash push -m "half-finished login form" ``` ## pop vs apply Both bring your changes back. The difference is what happens to the stash afterwards: ```bash git stash pop # restore the changes AND delete the stash entry git stash apply # restore the changes but KEEP the stash entry ``` Use `apply` when you want to restore the same changes onto more than one branch. Use `pop` for the normal "I'm back, give me my work" case. ## Working with multiple stashes List what you have stashed: ```bash git stash list ``` ```text stash@{0}: On main: half-finished login form stash@{1}: WIP on feature: 3f9a1c2 ... ``` Apply or pop a specific one by its reference: ```bash git stash apply stash@{1} git stash pop stash@{1} ``` Drop a stash you no longer need, or clear them all: ```bash git stash drop stash@{0} git stash clear ``` ## When pop hits a conflict If your stashed changes conflict with the current state of the files, `git stash pop` stops with merge conflicts and, importantly, **keeps the stash** so you do not lose it. Resolve the conflicts, stage the files, then drop the stash manually: ```bash git stash drop ``` If you would rather permanently discard uncommitted changes than stash them, see [git reset --hard](/git-reset-hard). ### What port does SSH use (and how to change it) Source: https://rocketeersapp.com/ssh-port SSH uses TCP port 22 by default. You can change it in the SSH daemon config, but the port itself is not a security feature, so know what changing it does and does not buy you. ## The default SSH port SSH listens on **TCP port 22** by default. That is the port your client connects to unless you tell it otherwise: ```bash ssh user@server.example.com # implicitly port 22 ssh -p 22 user@server.example.com # the same thing, spelled out ``` Port 22 is assigned to SSH by IANA, so it is what every client, firewall rule and hosting panel assumes. ## Connecting on a non-default port If a server runs SSH on another port, pass it with `-p` (lowercase) for `ssh`, and `-P` (uppercase) for `scp`: ```bash ssh -p 2222 user@server.example.com scp -P 2222 file.txt user@server.example.com:/tmp/ ``` To avoid typing it every time, set it in `~/.ssh/config`: ```text Host myserver HostName server.example.com User deploy Port 2222 ``` Then just `ssh myserver`. ## Changing the SSH port on the server Edit the SSH daemon config and set the `Port` directive: ```bash sudo nano /etc/ssh/sshd_config ``` ```text Port 2222 ``` Before restarting, open the new port in your firewall, or you will lock yourself out: ```bash sudo ufw allow 2222/tcp ``` On distributions with SELinux (RHEL, Rocky, AlmaLinux) you also have to register the port: ```bash sudo semanage port -a -t ssh_port_t -p tcp 2222 ``` Then restart the daemon: ```bash sudo systemctl restart ssh # or: sudo systemctl restart sshd ``` Keep your current session open and test the new port from a second terminal before logging out. If it works, you can remove the old `22/tcp` firewall rule. ## Does changing the port improve security? Moving off port 22 cuts down the noise in your logs from automated bots scanning the default port, but it is **security through obscurity**, not real protection. A port scan finds the new port in seconds. The changes that actually matter: - Use key-based authentication and set `PasswordAuthentication no`. - Disable direct root login with `PermitRootLogin no`. - Put the server behind a firewall and, ideally, only allow SSH from known IPs. If your key auth is misconfigured, see [SSH Permission denied (publickey)](/ssh-permission-denied-publickey). For the related "address already in use" problem when a port is taken, see [Address already in use (port already bound)](/address-already-in-use-port). ### How to rename a Git branch (local and remote) Source: https://rocketeersapp.com/git-rename-branch Renaming a branch locally is one command. The part people miss is updating the remote: Git cannot rename a remote branch directly, so you push the new name and delete the old one. ## Rename the branch you are on The `-m` (move) flag renames a branch. To rename the branch you currently have checked out, you only need the new name: ```bash git branch -m new-name ``` ## Rename a different branch To rename a branch without checking it out first, give both the old and new names: ```bash git branch -m old-name new-name ``` ## Push the rename to the remote A remote branch cannot be renamed in place. The workflow is: push the new name, then delete the old one. First push the renamed branch and set it as the upstream: ```bash git push origin -u new-name ``` Then delete the old branch from the remote: ```bash git push origin --delete old-name ``` If other people work on this branch, tell them: their local clones still track the old name and will need `git fetch --prune` to clean it up. ## Renaming the default branch The same steps apply to a default branch (for example renaming `master` to `main`), but you also have to update the default on your hosting platform (GitHub, GitLab) in the repository settings, and update any branch protection rules or CI configuration that referenced the old name. To delete a branch entirely rather than rename it, see [How to delete a local (and remote) Git branch](/git-delete-local-branch). ### Install PHP memcached extension on macOS Source: https://rocketeersapp.com/install-php-memcached-extension-on-macos ## Install using Homebrew and PECL If you have PHP installed using [Homebrew](https://brew.sh), you previously could also install PHP extensions using `brew` itself. But things have changed, and you need to use `pecl` to install additional extensions. When you need to install `memcached`, you need to install this including the dependencies `zlib` and `libmemcached`: ```bash brew install memcached libmemcached zlib pkg-config ``` Then you can initiate the install via `pecl`: ```bash pecl install memcached ``` This will ask multiple questions, where you will only need to answer the question about the path of zlib: ```bash zlib directory [no] : ``` This path depends on the processor of your Mac, there's a difference between Apple silicon machines on Mac Intel. The path is per type of machine: ## ZLIB path ```bash # On Apple Silicon /opt/homebrew/opt/zlib # On Mac Intel /usr/local/opt/zlib ``` When given this path, the `pecl` install should complete successfully. On a fresh Mac you'll also need the [Xcode command line tools](/install-xcode-command-line-tools) installed before you can build PHP extensions. ## Email ### Best practices for sending email Source: https://rocketeersapp.com/best-practices-sending-email Improve the emails you're sending from your website or web app by following these best practices.5555 ## First: Optimize deliverability First things first, make sure your [sending email technically correct following these tips](/improving-email-deliverability). Our free [SPF](/spf), [DKIM](/dkim), and [DMARC](/dmarc) checkers and the [deliverability tester](/deliverability) confirm the technical side is right. This way you'll make sure email is sent the right way your hard work on writing these emails isn't for nothing. ## Keep your design simple Compatibility with email clients is one of the most anoying things for sending email. It's the worst when you are designing a beautiful email or newsletter and seeing it destroyed by that corporately required Microsoft Outlook 2010 version. One way or another, designing emails is a pain. It still is possible, but brace yourself. So advice is, don't make it too difficult and keep emails calm and easy. ## Use inline styles for CSS For compatibility with all these different email clients it is advised to inline all your CSS styles. Using inline styles you can be sure that all styles are applied on your HTML elements. Because some clients strip `` and `` tags from the email. It's best to automate the inlining of styles in your code, just before sending the email. So you can still develop the email like you would using stylesheets and CSS selectors. This can be done using an [open source package for inlining CSS](https://github.com/topics/inline-css). Doing this manually is also an option, you can copy and paste your HTML in [this tool from Mailchimp](https://templates.mailchimp.com/resources/inline-css/) and it converts your HTML and separate CSS selectors to HTML with inline style attributes. ## Create bulletproof buttons [Buttons.cm](https://buttons.cm) is an indispensable tool when creating buttons for your emails. This tool provides all the necessary legacy code that is needed for all of the different variaty of Microsoft Outlook versions. ## Compatible background images Background images are another crazy difficult thing to achieve across email clients, also for this special feature you can use a HTML generator that creates cross compatible [image backgrounds in emails](https://backgrounds.cm). ## Write a decent subject line If you don't want to send your emails straight to the junk folder of your users, be thoughtful with what you put in the subject line of your emails. If you use sketchy phrases like _free_, _buy_ or _now !!!_ than you will end up in the spam because this is what spammers also use to trigger the attention of people. ## Add a preheader text A lot of email clients show an intro text right below the subject line to preview some of the contents of an email. This way the reader gets more information about what's inside the email. When you don't add a preheader text, the email client can show some random piece of text inside your email message which is not very professional looking. Using this snippet of HTML, you can add the intro text carefully inside the `
` to only be shown as a preheader text and not inside the email itself: ```html
``` ## Add an unsubscribe link If your sending email more than once, make sure you add a link for the recipient to unsubscribe from your emails. Because nobody that wants to receive your email, should receive your email and it's better to not have them on your list anymore. Big numbers aren't everything and you probably get more reliably sending statistics and better open rates because of this. ### Improving email deliverability Source: https://rocketeersapp.com/improving-email-deliverability How to prevent emails going to spam and improve the overall deliverability of your email domain. When you're sending email from your website or web app, nothing is more frustrating when you get notified by your customers that your emails aren't delivering or ending up in the junk folder. This can feel like it is not fully in your power to improve this, but when you execute all of these steps in this article, you will get better email delivery and you've done everything you could do! ## Do not ever use your own mail server Please do not fall in the trap of sending email using your own mail server or the SMTP server that comes with your regular mailbox. In the current digital age you can't fight the tech giants like Google, Microsoft and Apple. They will crush you when you try to send email using your own mailserver. By default they will not trust it and you need to send a lot of email continously to keep trust when you have obtained it (not likely). ## Use a trustworthy email service provider Choose an email sending service that is reliable and has a good reputation. In the following order from best to still pretty good, use one of these options: [Postmark](https://postmarkapp.com), [SendGrid](https://sendgrid.com), [Amazon SES](https://aws.amazon.com/ses/) or [Mailgun](https://mailgun.com). Added benefits to using one of these providers is that you get great insights in what happens with the email your sending. You can view all logs and events that happen before (hopefully) entering the receiving mailbox. Click and open tracking is also an option, but this harms the privacy of for your users. Be careful to not enable this without thinking this through. ## Configure DKIM To authenticate the sending server, you should configure DKIM correctly for your email service provider that you chose in the previous step. This way the receiving mail server can verify that the email indeed has been sent by the legitimate server you declared to use. ## Add a valid SPF record to your DNS In case your email provider provides a SPF record, you should add it to the DNS of your email domain. The SPF record should contain a list of all allowed domains or IPs that are allowed to send email with your domain. Postmark is known for not providing a SPF record, because they explain it's not required anymore because the Return-Path domain is now used to check for SPF alignment. ## Setup a DMARC policy To prevent email spoofing further, the protocol DMARC (Domain-based Message Authentication, Reporting, and Conformance) has been invented. This works together with SPF and DKIM for authentication of emails and is used to describe the actions taken when an email is not aligned with SPF or DKIM. Because the protocol is relatively new (2012) it is not so widely used as it should be. Because of this, DMARC is one of the best improvements you can make to your existing setup. Because it marks the quality of your overall setup and in comparison to other sending mail servers. In its most basic form you could add a DMARC record without much work like this, and still improve your email reputation by only 'having' this record: ``` v=DMARC1; p=none; pct=100; sp=none; aspf=r; ``` This practically says: we are testing the use of DMARC (p=none), for all email (pct=100), do not reject emails from subdomains (sp=none) and align relaxed with SPF (aspf=r). To make DMARC more strict (and useful), you should receive email reports to know if legit emails are not being blocked by your DMARC policy. Receiving and aggregating these reports can be a pain, so a service like [DMARC monitoring](https://dmarc.postmarkapp.com) could come in handy. This is how a more strict record looks like, which will reject all email that does not align with your SPF and DKIM settings: ``` v=DMARC1; p=reject; pct=100; rua=mailto:abc@dmarc.postmarkapp.com; sp=reject; aspf=s; adkim=s; ``` ## Prevent hard bounces Nothing will hurt your email domain reputation more than sending email to large lists of that contain a large percentage of not working email addresses. These (hard) bounces get noticed by the email providers like Google and Microsoft, because a large percentage of users are using their email service of choice and when they see a spike of bounces form your domain, they will give you a negative score based on these events. What you can do to prevent hard bounces: - Use email confirmation for newsletters and user registrations - Check email addresses before adding it to your list for common errors like typos, DNS errors and RFC spec validation - Clean unknown email lists before sending ([NeverBounce](https://neverbounce.com)) ## Write a decent subject line If you don't want to send your emails straight to the junk folder of your users, be thoughtful with what you put in the subject line of your emails. If you use sketchy phrases like _free_, _buy_ or _now !!!_ than you will end up in the spam because this is what spammers also use to trigger the attention of people. ## Add an unsubscribe link If your sending email more than once, make sure you add a link for the recipient to unsubscribe from your emails. Because nobody that wants to receive your email, should receive your email and it's better to not have them on your list anymore. Big numbers aren't everything and you probably get more reliably sending statistics and better open rates because of this. ## Test your email before sending it There are some handy tools that can check a lot of requirements for a good delivery rate of your email. [Mail Tester](https://www.mail-tester.com) is one of these tools that can be a great help. Go to the website, copy the provided email address and send your email to this address. After a few seconds Mail Tester can show what errors are still in your email delivery. Make sure you get the maximum score before proceeding to sending it to your email list. Get the underlying records right with our free [SPF](/spf), [DKIM](/dkim), and [DMARC](/dmarc) checkers, and confirm your server isn't on a [blacklist](/blacklist). For the writing and formatting side, see [best practices for sending email](/best-practices-sending-email). ## Errors ### CORS error: No Access-Control-Allow-Origin header Source: https://rocketeersapp.com/cors-error-no-access-control-allow-origin A CORS error means the browser blocked a cross-origin request because the server did not return the right headers. The fix is to send Access-Control-Allow-Origin from the API, not to disable browser security. ## About the error In the browser console you see something like: ```bash Access to fetch at 'https://api.example.com/users' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. ``` CORS (Cross-Origin Resource Sharing) is a browser security mechanism. When your frontend on one origin calls an API on another origin, the browser only exposes the response if the API explicitly allows that origin via response headers. No header, blocked request. ## Why do I see this error - The API doesn't send an `Access-Control-Allow-Origin` header. - The frontend and API are on different origins (different domain, subdomain, port, or scheme). - A **preflight** `OPTIONS` request (sent for non-simple requests) isn't being answered correctly. - Credentials (cookies) are involved but the headers don't permit them. Note the error is reported by the browser. The request often reaches your server fine, the browser just hides the response from your JavaScript. ## Solution ### Laravel Laravel has built-in CORS handling. Configure the allowed origins in `config/cors.php`: ```php 'paths' => ['api/*', 'sanctum/csrf-cookie'], 'allowed_methods' => ['*'], 'allowed_origins' => ['https://app.example.com'], 'allowed_headers' => ['*'], 'supports_credentials' => true, ``` Set `supports_credentials` to `true` only if you send cookies, and in that case `allowed_origins` cannot be `*`, it must list explicit origins. Clear config after editing: ```bash php artisan config:clear ``` ### nginx If you serve the API directly through nginx, add the headers in the relevant `location` block and answer the preflight `OPTIONS` request: ```nginx location /api/ { add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always; if ($request_method = OPTIONS) { return 204; } } ``` ### Don't "fix" it in the browser Disabling web security with a browser flag or a proxy extension only hides the error on your machine, every real visitor still gets blocked. CORS must be solved on the server that owns the API. For other browser-facing errors, see [500 Internal Server Error](/500-internal-server-error-laravel) and [ERR_TOO_MANY_REDIRECTS](/err-too-many-redirects). ### curl (60) SSL certificate problem: unable to get local issuer certificate Source: https://rocketeersapp.com/curl-60-ssl-certificate-problem-unable-to-get-local-issuer-certificate This curl error means it could not verify the remote server certificate against a trusted root. Usually the local CA bundle is outdated or missing, not a problem with the remote site. ## About the error The message reads: ```bash curl: (60) SSL certificate problem: unable to get local issuer certificate ``` curl connected over TLS but couldn't build a trust chain from the server's certificate up to a root certificate it knows. To verify a certificate, curl needs the issuing CA certificates available locally. If it can't find them, it errors out rather than trusting blindly. ## Why do I see this error - The system's CA certificate bundle is outdated or missing. - The server doesn't send its full chain, so an intermediate [certificate in the chain](/what-is-an-ssl-certificate-chain) is absent. - The certificate (or an intermediate) has expired. - The machine's clock is wrong, certificates are time-sensitive, so a bad system time breaks verification. ## Solution ### Update the CA bundle (the right fix) On Debian or Ubuntu: ```bash sudo apt update sudo apt install --reinstall ca-certificates sudo update-ca-certificates ``` On RHEL, CentOS or Fedora: ```bash sudo yum reinstall ca-certificates sudo update-ca-trust ``` This refreshes the trusted roots and resolves the error in the vast majority of cases. ### Point curl at a specific CA bundle If the certificates are installed but curl still can't find them, tell it where to look: ```bash curl --cacert /etc/ssl/certs/ca-certificates.crt https://example.com ``` For PHP's curl, set the path in `php.ini` so every request uses it: ```ini curl.cainfo = "/etc/ssl/certs/ca-certificates.crt" openssl.cafile = "/etc/ssl/certs/ca-certificates.crt" ``` ### Diagnose with verbose output To see exactly where the chain breaks: ```bash curl -v https://example.com ``` ### Do not disable verification You'll see advice to use `curl -k` (or `CURLOPT_SSL_VERIFYPEER = false` in code). That turns off certificate verification entirely and exposes you to man-in-the-middle attacks. Fix the trust store instead. If you're chasing other curl trouble on older servers, see [error in the HTTP/2 framing layer](/error-in-the-http2-framing-layer). ### 502 Bad Gateway in nginx Source: https://rocketeersapp.com/502-bad-gateway-nginx A 502 Bad Gateway means nginx reached your application but got an invalid response back. In almost every case the backend (PHP-FPM) crashed, never started, or is unreachable. ## About error 502 A `502 Bad Gateway` is returned by nginx when it is acting as a reverse proxy and receives an invalid response from the upstream server it forwards requests to. For a PHP application that upstream is almost always **PHP-FPM**. The important thing to understand: nginx itself is fine. The problem sits *behind* nginx, in the service it is trying to talk to. ## Why do I see this error The most common causes, roughly in order: - PHP-FPM is not running, crashed, or was never started. - nginx points to the wrong PHP-FPM socket or port (a typo in `fastcgi_pass`). - The socket file exists but the nginx user can't read it (wrong permissions). - The backend died halfway through the request (often an out of memory kill). Always start by reading the nginx error log, it names the exact reason: ```bash tail -f /var/log/nginx/error.log ``` You'll see a line ending in something like `connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory)` or `(111: Connection refused)`. That bracketed code tells you everything. ## Solution First, check whether PHP-FPM is actually running and start it if not: ```bash systemctl status php8.3-fpm systemctl restart php8.3-fpm ``` Then confirm nginx is pointing at the socket that PHP-FPM actually listens on. The path in your nginx config must match the `listen` directive in the pool config (`/etc/php/8.3/fpm/pool.d/www.conf`): ```nginx location ~ \.php$ { fastcgi_pass unix:/run/php/php8.3-fpm.sock; include fastcgi_params; } ``` If the socket exists but you still get `(13: Permission denied)`, make sure the pool runs as the same user as nginx (usually `www-data`): ```ini user = www-data group = www-data listen.owner = www-data listen.group = www-data ``` Reload both services after any change: ```bash systemctl reload php8.3-fpm systemctl reload nginx ``` If the backend keeps dying mid-request rather than refusing the connection, the cause is usually resources, not configuration. Check the [504 Gateway Timeout](/504-gateway-timeout-nginx) article for slow responses, and [PHP allowed memory size exhausted](/php-allowed-memory-size-exhausted) for out of memory kills. When the backend is up but every worker is busy, you'll see a [503 Service Unavailable](/503-service-unavailable) instead. A backend returning oversized response headers triggers a different upstream error: [upstream sent too big header](/nginx-upstream-sent-too-big-header). ### 403 Forbidden in nginx Source: https://rocketeersapp.com/403-forbidden-nginx A 403 Forbidden means nginx found the resource but refuses to serve it. Almost always a file permission problem, a missing index file, or an explicit deny rule. ## About error 403 A `403 Forbidden` is returned when nginx successfully locates the request but is not allowed to serve it. Unlike a 404, the resource exists, nginx just won't hand it over. ## Why do I see this error The usual causes, in rough order of likelihood: - File or directory permissions the nginx worker user can't read or traverse. - The URL points at a directory with no index file and `autoindex` off. - An explicit `deny` rule in the configuration. - Wrong ownership on the document root after a deploy. As always, the nginx error log names the exact reason: ```bash tail -f /var/log/nginx/error.log ``` You'll see lines like `directory index of "/var/www/html/" is forbidden` or `Permission denied`. ## Solution ### Fix permissions nginx must be able to *read* files and *traverse* (execute bit) every directory in the path. Directories should be `755` and files `644`: ```bash sudo find /var/www/html -type d -exec chmod 755 {} \; sudo find /var/www/html -type f -exec chmod 644 {} \; ``` Ownership should match the web server user (`www-data` on Debian/Ubuntu, `nginx` on RHEL): ```bash sudo chown -R www-data:www-data /var/www/html ``` For a Laravel app the web root is the `public` directory, and only `storage` and `bootstrap/cache` need to be writable, see [Laravel failed to open stream: Permission denied](/laravel-failed-to-open-stream-permission-denied). ### Missing index file If the request is for a directory, make sure an index file is defined and present: ```nginx index index.php index.html; ``` Avoid turning on `autoindex on;` on a public server, it exposes your file listing. ### Check for deny rules Search your config for an explicit block you may have forgotten: ```nginx location ~ /\.(?!well-known) { deny all; } ``` That example (blocking dotfiles) is correct, but an over-broad `deny` is a common self-inflicted 403. Reload after any change: ```bash nginx -t && systemctl reload nginx ``` If the document root or PHP socket itself is misconfigured rather than just permissions, start from a clean [Nginx install](/how-to-install-nginx). A bad config can just as easily produce a [502 Bad Gateway](/502-bad-gateway-nginx) instead. ### SSL handshake failed in nginx (ERR_SSL_PROTOCOL_ERROR) Source: https://rocketeersapp.com/ssl-handshake-failed-nginx A failed TLS handshake means the browser and nginx could not agree on a secure connection. The top causes are an expired certificate and a missing intermediate chain. ## About the error The visitor sees `ERR_SSL_PROTOCOL_ERROR` or "SSL handshake failed", and nginx logs an SSL error. The handshake is the negotiation that happens before any data is exchanged, if it fails, the page never loads over HTTPS. ## Why do I see this error In production, almost always one of these: - **Expired certificate**, the single most common cause. - **Missing intermediate certificate**, nginx doesn't auto-fetch the chain, you must bundle it. - **Wrong certificate or key path** in the config, or a key that doesn't match the certificate. - **Outdated protocol**, the client requires TLS 1.2+ and the server only offers older versions. ## Diagnose Test the live handshake with our free [SSL checker](/ssl), or with OpenSSL. The `-servername` flag sends SNI, which is required when one IP serves multiple certificates: ```bash openssl s_client -connect example.com:443 -servername example.com ``` Check the certificate's expiry date directly: ```bash echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates ``` And read the nginx error log: ```bash tail -f /var/log/nginx/error.log ``` ## Solution ### Renew an expired certificate With Certbot: ```bash sudo certbot renew sudo systemctl reload nginx ``` Automate renewal so it never expires again, Certbot installs a timer for this by default. ### Serve the full chain The `ssl_certificate` directive must point at the **full chain** (your certificate followed by the intermediates), not just the leaf certificate: ```nginx ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ``` A leaf-only certificate works in some browsers and fails in others, a classic intermittent handshake failure. ### Enforce modern protocols ```nginx ssl_protocols TLSv1.2 TLSv1.3; ``` Validate and reload after any change: ```bash nginx -t && systemctl reload nginx ``` For a hardened SSL setup behind a CDN, see [an A+ grade SSL using Cloudflare](/a-plus-grade-ssl-using-cloudflare). If the chain problem shows up in command-line tools rather than browsers, see [curl (60) SSL certificate problem](/curl-60-ssl-certificate-problem-unable-to-get-local-issuer-certificate). When these same problems reach a visitor's browser, they see [your connection is not private](/your-connection-is-not-private) or [NET::ERR_CERT_AUTHORITY_INVALID](/net-err-cert-authority-invalid). ### Your connection is not private Source: https://rocketeersapp.com/your-connection-is-not-private This browser warning means the SSL certificate could not be validated. As a site owner it usually points at an expired certificate, a missing chain, or a domain mismatch you can fix on the server. ## About the error Chrome shows a full-page "Your connection is not private" warning with a code such as `NET::ERR_CERT_*`. The browser couldn't validate the site's SSL certificate, so it blocks access to protect the visitor. If it's your own site, it means visitors are being turned away, so it's worth fixing fast. ## Why do I see this error The specific code under the warning tells you which problem it is: - **`NET::ERR_CERT_DATE_INVALID`** the certificate has expired (the most common). - **`NET::ERR_CERT_AUTHORITY_INVALID`** untrusted issuer or missing chain, see [NET::ERR_CERT_AUTHORITY_INVALID](/net-err-cert-authority-invalid). - **`NET::ERR_CERT_COMMON_NAME_INVALID`** the certificate doesn't cover the domain being visited (e.g. `www` missing). - A wrong **system clock** on the visitor's device (the one cause that isn't your fault). ## Solution ### Renew an expired certificate Expiry is the number one cause. With Certbot, renew and make sure auto-renewal is active so it never lapses again: ```bash sudo certbot renew sudo systemctl reload nginx ``` Check the expiry date directly, or use our free [SSL checker](/ssl) and the guide on [how to check SSL certificate expiration](/check-ssl-certificate-expiration): ```bash echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates ``` ### Cover every domain the site answers on A `COMMON_NAME_INVALID` error means the certificate is missing a name. Issue it for both the apex and `www` (and any subdomains you serve): ```bash sudo certbot --nginx -d example.com -d www.example.com ``` ### Serve the full chain If the code is `AUTHORITY_INVALID`, point nginx at `fullchain.pem`, not the leaf-only `cert.pem`, so the browser can build the trust chain: ```nginx ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ``` Then validate and reload: ```bash nginx -t && systemctl reload nginx ``` For the underlying TLS negotiation failures behind these warnings, see [SSL handshake failed in nginx](/ssl-handshake-failed-nginx), and for a hardened setup, [an A+ grade SSL using Cloudflare](/a-plus-grade-ssl-using-cloudflare). ### 504 Gateway Timeout in nginx Source: https://rocketeersapp.com/504-gateway-timeout-nginx A 504 Gateway Timeout means nginx reached your application, but the application took too long to respond. Unlike a 502, the backend is alive, it is just slow. ## About error 504 A `504 Gateway Timeout` is returned when nginx, acting as a reverse proxy, successfully connects to the upstream server but does not receive a complete response within the configured timeout window. This is the key difference with a [502 Bad Gateway](/502-bad-gateway-nginx): with a 502 the backend gave a bad or no connection, with a 504 the connection was fine but the response never arrived in time. ## Why do I see this error The backend is taking longer than nginx is willing to wait. Common reasons: - A slow database query or a missing index. - A long running job (report generation, export, image processing) handled inside the request instead of a queue. - A slow external API call the request depends on. - PHP's own `max_execution_time` is lower than the work needs. By default nginx waits 60 seconds (`proxy_read_timeout` for proxied apps, `fastcgi_read_timeout` for PHP-FPM) before giving up. ## Solution For a PHP-FPM application, raise the FastCGI read timeout in your `server` or `location` block: ```nginx location ~ \.php$ { fastcgi_pass unix:/run/php/php8.3-fpm.sock; fastcgi_read_timeout 120s; include fastcgi_params; } ``` For a proxied application (Node, Octane, a separate service), raise the proxy timeouts instead: ```nginx location / { proxy_pass http://127.0.0.1:8000; proxy_read_timeout 120s; proxy_send_timeout 120s; } ``` PHP has its own limit too, so raise that as well or nginx will outwait a script that PHP already killed: ```ini ; php.ini max_execution_time = 120 ``` Reload after changing the configuration: ```bash systemctl reload nginx systemctl reload php8.3-fpm ``` Raising timeouts is a bandage, not a cure. The real fix is to make the request fast: add the missing database index, or move slow work (emails, exports, third-party calls) into a background queue so the request returns immediately. See [optimizing server performance](/optimize-server-performance) for more. ### 503 Service Unavailable Source: https://rocketeersapp.com/503-service-unavailable A 503 means the server is temporarily unable to handle the request. With Laravel it often means maintenance mode is on; on a busy server it means PHP-FPM has no free workers. ## About error 503 A `503 Service Unavailable` signals that the server is up but temporarily can't serve the request. It's meant to be transient. There are two very different situations that produce it, and the fix depends on which one you're in. ## Cause 1: Laravel maintenance mode If you (or your deploy script) ran `php artisan down`, Laravel returns a 503 for every request on purpose. Bring it back up with: ```bash php artisan up ``` If a deploy crashed midway and left the app down, the same command fixes it. You can also allow your own IP through while it's down: ```bash php artisan down --secret="let-me-in" ``` Then visit `/let-me-in` once to bypass the maintenance page. ## Cause 2: PHP-FPM has no free workers On a busy server, a 503 (often paired with `502`) means PHP-FPM hit its process limit and nginx had nowhere to send the request. Check the PHP-FPM log for the tell-tale warning: ```bash tail -f /var/log/php8.3-fpm.log ``` You'll see `server reached pm.max_children setting, consider raising it`. ### Solution Raise the worker pool in your pool config (`/etc/php/8.3/fpm/pool.d/www.conf`), sizing it to your available RAM: ```ini pm = dynamic pm.max_children = 20 pm.start_servers = 4 pm.min_spare_servers = 2 pm.max_spare_servers = 8 ``` Each PHP worker uses real memory (often 30–60 MB), so don't set `max_children` higher than `RAM / per-worker memory`. Reload after changing it: ```bash systemctl reload php8.3-fpm ``` If workers are exhausted because requests are slow rather than because traffic is genuinely high, fix the slowness instead, see [504 Gateway Timeout](/504-gateway-timeout-nginx) and [optimizing server performance](/optimize-server-performance). If the 503 comes from a rate limit you configured, see [nginx rate limiting](/nginx-rate-limiting). ### NET::ERR_CERT_AUTHORITY_INVALID Source: https://rocketeersapp.com/net-err-cert-authority-invalid This browser error means the SSL certificate was not issued by a trusted authority, or the chain is incomplete. Usually a self-signed certificate, a missing intermediate, or an untrusted CA. ## About the error Chrome shows `NET::ERR_CERT_AUTHORITY_INVALID` behind a "Your connection is not private" warning. The browser received a certificate it can't trace back to a Certificate Authority it trusts, so it refuses to proceed. ## Why do I see this error - A **[self-signed certificate](/generate-self-signed-certificate-openssl)** (common in local dev and on staging). - A **missing intermediate certificate**, the leaf is valid but the browser can't build the chain to a trusted root. - A certificate from an **untrusted or unknown CA**. - A certificate that doesn't match the domain, or has expired (often a slightly different error, but related). ## Solution ### Serve the full certificate chain This is the most common production cause. nginx does not fetch intermediates for you, so `ssl_certificate` must point at the **full chain** (leaf + intermediates), not just your domain's certificate: ```nginx ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ``` Using `fullchain.pem` (not `cert.pem`) is what fixes the "authority invalid" error for an otherwise valid Let's Encrypt certificate. Reload after changing it: ```bash nginx -t && systemctl reload nginx ``` ### Verify the chain Check what the server actually sends. A complete chain shows the intermediate; a broken one stops at your leaf: ```bash openssl s_client -connect example.com:443 -servername example.com -showcerts ``` ### Use a real certificate (not self-signed) in production If this is a public site, issue a free, trusted certificate with Certbot instead of a self-signed one: ```bash sudo certbot --nginx -d example.com -d www.example.com ``` ### Local development For a local self-signed certificate the warning is expected. Use a tool that installs a locally-trusted CA (such as Laravel Valet's TLS, or `mkcert`) rather than clicking through the warning every time. ### If only one device sees the error When the site loads fine for everyone else, the problem is local to that machine, not the server: - **Wrong date and time.** Certificate validation depends on the system clock. Correct the date, time and timezone, then reload the page. - **Antivirus or firewall HTTPS scanning.** Security software that inspects encrypted traffic substitutes its own certificate. Turn off HTTPS/SSL scanning in the tool, or let it install its trusted CA. - **A corporate proxy** performing TLS interception. Your network administrator needs to deploy the proxy's root certificate to the device's trust store. This is the browser-facing cousin of two server-side TLS errors: [SSL handshake failed in nginx](/ssl-handshake-failed-nginx) and [curl (60) SSL certificate problem](/curl-60-ssl-certificate-problem-unable-to-get-local-issuer-certificate). ### ERR_TOO_MANY_REDIRECTS (redirect loop) Source: https://rocketeersapp.com/err-too-many-redirects This error means the browser was bounced between URLs until it gave up. The classic cause is a redirect loop between HTTP and HTTPS, very often a Cloudflare SSL setting fighting your server config. ## About the error The browser shows `ERR_TOO_MANY_REDIRECTS` (Chrome) or "The page isn't redirecting properly" (Firefox). It means a URL redirected to another URL that redirected back, forming a loop. After a handful of hops the browser stops to avoid looping forever. ## Why do I see this error The overwhelmingly common cause is an **HTTP ↔ HTTPS loop**, and the most common trigger of that is **Cloudflare's SSL mode set to "Flexible"**: - Cloudflare talks to your server over plain HTTP. - Your server (or app) redirects all HTTP to HTTPS. - Cloudflare receives that redirect, requests again over HTTP, gets redirected again, forever. Other causes: an app forcing HTTPS while a proxy already terminates TLS, a misconfigured `www` ↔ non-`www` redirect, or two redirect rules pointing at each other. ## Solution ### Fix Cloudflare SSL mode If you use [Cloudflare](/what-is-cloudflare), set the SSL/TLS encryption mode to **Full** or **Full (strict)**, never **Flexible**. Flexible is the single biggest cause of this loop for sites behind Cloudflare. Full means Cloudflare connects to your origin over HTTPS, which matches your server redirecting to HTTPS. See [an A+ grade SSL using Cloudflare](/a-plus-grade-ssl-using-cloudflare). ### Trust the proxy's protocol header When TLS is terminated by a proxy or load balancer, your app sees the request as plain HTTP and redirects to HTTPS, even though the visitor is already on HTTPS. Tell the app to trust the forwarded protocol. In Laravel, configure trusted proxies in `bootstrap/app.php`: ```php ->withMiddleware(function (Middleware $middleware) { $middleware->trustProxies(at: '*', headers: Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO ); }) ``` ### Check your nginx redirect A correct HTTP→HTTPS redirect redirects only plain HTTP, and the HTTPS server block must not redirect again: ```nginx server { listen 80; server_name example.com; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name example.com; # serve the site here, do NOT redirect to https again } ``` nginx can also loop internally, without the browser ever seeing a redirect, that's the [rewrite or internal redirection cycle](/nginx-rewrite-or-internal-redirection-cycle) error in the nginx error log. ### Diagnose the loop Follow the redirect chain with [curl](/curl-command-complete-guide) to see exactly where it loops: ```bash curl -sIL https://example.com | grep -i location ``` If you see the same two URLs alternating, you've found your loop. ### If it isn't your website When you hit this on a site you don't control, the fix is on your end: delete that site's cookies and reload. A stale or corrupt session cookie is the usual culprit, and an incognito window (which starts with no cookies) is the fastest way to confirm it. If the page loads in incognito, clear the cookies in your normal window. ### 413 Request Entity Too Large in nginx Source: https://rocketeersapp.com/413-request-entity-too-large When uploading files you can encounter this error, which is caused by a limit in the nginx configuration. ## About error 413 The error with response code 413 shows up as "Request Entity Too Large" in the error logs of nginx and "Payload Too Large" in the developer console of your browser. Other ways of telling you about this same error could be "Content Too Large" or "Requested content-length of ... is larger than the configured limit of ...". ## Why do I see this error This error happens when the uploaded file is larger than the configured maximum body size in nginx. Therefore the solution is to increase this limit. ## Solution This is set in your nginx config; if you don't yet have one, see [how to install Nginx](/how-to-install-nginx). By increasing the `client_max_body_size` in nginx, we can make sure the uploaded files are accepted. This can be done in the `nginx.conf` file, or in the `sites-available` configuration file of your website: ```bash client_max_body_size 100M; ``` You can set the limit using these units: ```bash ms # milliseconds s # seconds m # minutes h # hours d # days w # weeks M # months, 30 days y # years, 365 days ``` ### Error in the HTTP2 framing layer Source: https://rocketeersapp.com/error-in-the-http2-framing-layer Once in a while I got this error when connecting to an external service on servers running Ubuntu 18.x or 20.x. After some research I got this flaky bug resolved! ## Root cause of the error We have a lot of servers running on [Digitalocean](https://m.do.co/c/0801ad4bd810) that sends email using an email service provider like Postmark or Mailgun. And once in every 4-5 emails that were send, I got this error message stating the error "Error in the HTTP2 framing layer" following the URL of the API it was connecting to. At first I thought the problem was on the API service I was using, because of a drop in the SSL connection or something. But after some research online, it wasn't a third party problem. But a problem on the server itself, and specifically a bug in the version of [curl](/curl-command-complete-guide) available in the APT repositories of Ubuntu. It hit our servers on Ubuntu 18.x and 20.x, so [check your Ubuntu version](/check-ubuntu-version-command-line) to see if you're likely affected. ## Solution The solution was to upgrade `curl` to the newest possible version. Because this new version is not offered through APT, we need to do it manually. ## How to upgrade curl manually Here's to install the latest `curl` version (v8.2.1) at the time of writing this article. ```bash # Remove installed curl version apt remove curl -y apt purge curl -y # Install libs and compile tooling apt-get update apt-get install -y libssl-dev autoconf libtool makez # Download new curl version, unzip, configure and compile build cd /usr/local/src wget https://curl.haxx.se/download/curl-8.2.1.zip unzip curl-8.2.1.zip cd /usr/local/src/curl-8.2.1 autoreconf -fi ./configure --with-ssl make make install # Install curl globally mv /usr/local/src/curl-8.2.1/src/.libs/curl /usr/local/bin/curl sudo ldconfig ``` When done, make sure you restart the services using `curl` on the server so it will use this new version. If curl complains about certificates instead of framing, that's a different problem: [curl (60) SSL certificate problem](/curl-60-ssl-certificate-problem-unable-to-get-local-issuer-certificate). ## Git ### GitHub Permission denied (publickey) Source: https://rocketeersapp.com/github-permission-denied-publickey When git clone, pull or push over SSH fails with this error, GitHub did not accept your SSH key. Usually the key is not generated, not added to your account, or not loaded into the agent. ## About the error ```bash git@github.com: Permission denied (publickey). fatal: Could not read from remote repository. ``` You're connecting to GitHub over SSH, and GitHub rejected the authentication because none of the keys your client offered matches a key on your account. ## Why do I see this error - You haven't generated an SSH key yet. - The public key isn't added to your GitHub account. - The key exists but isn't loaded into the SSH agent. - You're using an HTTPS-style setup but a remote that expects SSH (or vice versa). ## Solution ### Test the connection GitHub has a dedicated endpoint for this. A success looks like a greeting, not a shell: ```bash ssh -T git@github.com ``` If it says `Hi !`, your key works and the problem is the repository remote, not auth. If it says `Permission denied`, continue below. ### Generate a key if you don't have one ```bash ssh-keygen -t ed25519 -C "you@example.com" ``` Press enter to accept the default location (`~/.ssh/id_ed25519`). ### Add the public key to GitHub Copy the **public** key (`.pub`) and paste it into GitHub under Settings → SSH and GPG keys → New SSH key: ```bash cat ~/.ssh/id_ed25519.pub ``` ### Load the key into the agent ```bash eval "$(ssh-agent -s)" ssh-add ~/.ssh/id_ed25519 ``` ### Wrong remote protocol If your account is set up for SSH but the repo was cloned over HTTPS (or the reverse), switch the remote URL: ```bash git remote set-url origin git@github.com:you/repo.git ``` For the general server-side version of this error (your own servers rather than GitHub), see [SSH Permission denied (publickey)](/ssh-permission-denied-publickey). New to SSH keys? See [what is an SSH key](/what-is-an-ssh-key) and [how to generate one](/generate-ssh-key). ### Git fatal: refusing to merge unrelated histories Source: https://rocketeersapp.com/git-refusing-to-merge-unrelated-histories This error means Git found no common ancestor between the two branches you are merging. It is a safety check, and there is a one-flag override once you are sure the merge is intentional. ## About the error ```bash fatal: refusing to merge unrelated histories ``` Introduced in Git 2.9, this is a guard rail. When you merge or pull, Git looks for a shared commit the two histories descend from. If there isn't one, it refuses rather than stitch together two genuinely separate project histories by accident. ## Why do I see this error - You created a repository locally (with its own commits) and then tried to pull from a remote that was also initialised separately, two roots, no shared ancestor. - You're merging a branch that was started from scratch rather than branched off the others. - The `.git` directory was deleted and recreated, so Git lost the shared history it used to know about. ## Solution If you've confirmed the merge really is intended (you genuinely want to combine these two histories), pass the override flag: ```bash git pull origin main --allow-unrelated-histories ``` Or, if you're merging a local branch: ```bash git merge other-branch --allow-unrelated-histories ``` Git will create a merge commit joining the two roots, and from then on they share history, so you only need the flag once. ### Before you use the flag The error is often a sign something is off, so it's worth a sanity check first: - Are you pushing to the **right remote**? A typo'd remote URL pointing at an unrelated repo triggers this. - Did you mean to **clone** instead of init + pull? If the remote already has the project, cloning it fresh avoids the whole situation. ```bash git remote -v # confirm the remote points where you expect ``` If the remote is wrong, fix it rather than forcing the merge: ```bash git remote set-url origin git@github.com:you/correct-repo.git ``` For the related "your push was rejected" situation, see [Git updates were rejected (non-fast-forward)](/git-updates-were-rejected-non-fast-forward). And if a botched merge leaves Git reporting it is [not a git repository](/git-not-a-git-repository), that guide covers the recovery. ### Git: Updates were rejected (non-fast-forward) Source: https://rocketeersapp.com/git-updates-were-rejected-non-fast-forward A rejected push means the remote branch has commits you do not have locally. Git refuses to overwrite them. The fix is to integrate the remote changes first, not to force-push. ## About the error ```bash ! [rejected] main -> main (non-fast-forward) error: failed to push some refs to '...' hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. ``` A push can only "fast-forward" when your local branch is a direct continuation of the remote one. If someone else (or another machine of yours) pushed commits in the meantime, your branch has diverged, and Git rejects the push to avoid silently discarding their work. ## Why do I see this error - A teammate pushed to the same branch before you. - You pushed from another clone or worktree and forgot. - The remote branch was amended or rebased, so the histories no longer line up. ## Solution ### Integrate the remote commits, then push Fetch and merge the remote changes into your branch first: ```bash git pull origin main git push origin main ``` If you prefer a linear history without merge commits, rebase your work on top of theirs instead: ```bash git pull --rebase origin main git push origin main ``` Resolve any conflicts that come up, then push, this time it fast-forwards cleanly. ### Do not reach for --force It's tempting to "fix" the rejection with `git push --force`, but on a shared branch that **deletes the commits the remote had that you didn't**, throwing away your teammate's work. Avoid it. If you genuinely need to overwrite (for example, after intentionally rebasing your *own* feature branch), use the safer variant, which refuses if the remote moved in a way you haven't seen: ```bash git push --force-with-lease ``` `--force-with-lease` aborts instead of clobbering if someone pushed since your last fetch, making it far safer than a blind `--force`. The related history error is covered in [Git fatal: refusing to merge unrelated histories](/git-refusing-to-merge-unrelated-histories). ### Git fatal: not a git repository Source: https://rocketeersapp.com/git-not-a-git-repository This error means you ran a git command somewhere git does not recognise as a repository. Usually you are in the wrong directory, or the repo was never initialised. ## About the error ```bash fatal: not a git repository (or any of the parent directories): .git ``` Git commands operate on a repository, identified by a `.git` directory. When you run a git command, git looks in the current directory and walks up through the parents searching for `.git`. If it never finds one, you get this error. ## Why do I see this error - You're in the **wrong directory**, outside the project, or one level above it. - The directory was never initialised as a git repository. - The `.git` directory was deleted (sometimes accidentally, sometimes by an over-broad clean). - You cloned into a subdirectory and are running git from the parent. ## Solution ### Check where you are First confirm your location and whether a `.git` directory exists here: ```bash pwd ls -la # look for a .git entry ``` If you don't see `.git`, you're either in the wrong place or this isn't a repo. `cd` into the actual project directory: ```bash cd ~/Sites/my-project git status ``` ### Initialise a new repository If this directory *should* be a repository but never was, create one: ```bash git init ``` ### Clone instead, if the project lives on a remote If the code already exists on GitHub and you just don't have it locally, clone it rather than init: ```bash git clone git@github.com:you/my-project.git cd my-project ``` ### Find the repository root from a subdirectory If you're somewhere inside a repo but a parent got confused, this prints the top level (or errors if you're truly outside one): ```bash git rev-parse --show-toplevel ``` If you recently saw [refusing to merge unrelated histories](/git-refusing-to-merge-unrelated-histories) and then this, a deleted or recreated `.git` directory is a likely common cause. ### Change casing of file or directory in Git Source: https://rocketeersapp.com/change-casing-of-file-or-directory-in-git Renaming already commited files or directories only for casing in Git will be ignored and will not show up as a change you can commit. ## The problem Sometimes you encouter the problem that a file or directory does not have the correct case-sensitive name it should have. So you rename the file and change its casing. After this change, Git does not show this and the renaming can't be commited. It is the same family of problem as [removing tracked files that should have been ignored](/removing-tracked-files-git). So this is not enough: `mv File.php file.php` ## The reason Since Git version 1.5.6 there is a setting `core.ignorecase` that defines if casing should be ignored by default or not. To change the default setting to pick up casing changes, you can run inside the project: ``` git config core.ignorecase false ``` Or to disable it completely on your computer, you can set it globally using: ``` git config --global core.ignorecase false ``` ## The fix (without changing the config) To fix this issue without changing any Git configurations: after you execute the `mv` command line from before, you also need to tell git that the filename has changed. This can be done by using: ``` git mv --cached File.php file.php ``` ### Removing tracked files in Git that should have been ignored Source: https://rocketeersapp.com/removing-tracked-files-git Removing files that has been committed earlier in your repository is something you encouter once in a while. Read here how you can do this. ## The problem Anybody that has been using Git for a longer period of time, stumbles every now and then on this pesky little problem: tracked files in your Git repository that should have been ignored from te start. But now they are added, Git can't seem to "forget" about them. I know I have faced this one a bunch of times! And here's how you can fix this issue and make you feel all clean and happy about your repository again. ## Delete and forget unwanted committed files In this example we have the common situation where you've forgot to add `.DS_Store` to your global or repositories `.gitignore` file on your Mac (for Windows users there is a different but evenly annoying `Thumbs.db` that gets created now and then). ```bash find . -name .DS_Store -print0 | xargs -0 git rm -f --ignore-unmatch ``` What does this command do? 1. First it deletes every file that matches `.DS_Store`; 2. Then it pipes these files to the `git rm` command that makes sure Git doesn't keep tracking these files. Without removing the traces of these files in Git, the `.DS_Store` files that were tracked will eventually pop up again in your working copy. For a related Git tracking quirk — renames that only change letter case — see [changing the casing of a file or directory in Git](/change-casing-of-file-or-directory-in-git). ## Hosting ### Improving PHP performance Source: https://rocketeersapp.com/php-performance The performance and speed of PHP can be improved in different ways. Find out the multiple pathways to better performance. ## Enable OPcache Using the OPcache PHP extension is one of the most significant (and easy) things you can do to improve PHP performance. When a PHP script runs, the server translates the script into a form the computer can understand (called "opcode") every time someone visits the website. This process takes time. With the OPcache extension enabled thee translated version (opcode) is stored in memory, so the server doesn’t have to re-translate the script every time. Instead, it reuses the stored version, making the website load faster. ```php # Install OPcache extension (change PHP version accordingly) sudo apt install php8.4-opache # Enable it in php.ini opcache.enable = 1 opcache.enable_cli = 1 opcache.max_accelerated_files = 50000 opcache.interned_strings_buffer = 64 opcache.memory_consumption = 256 opcache.save_comments = 1 opcache.validate_timestamps = 1 ``` If you change `opcache.validate_timestamps` to `0` you can improve performance even more because OPcache does not need to check the file for modifications everytime. But then you will need to make sure you reload the PHP process every time you make changes in your code. ## Enable OPcache JIT compiler After enabling OPcache, you should also consider enabling the [OPcache JIT compiler](/php-8-opcache-jit). It enhances performance by translating frequently executed PHP code into machine code at runtime, meaning running even more efficient on your server. Enable JIT by adding these lines to the OPcache config: ```php opcache.jit=on opcache.jit_buffer_size=128M ``` ### Website launch checklist Source: https://rocketeersapp.com/website-checklist A practical checklist of everything to verify before you put a website live — security, performance, backups, and the things that are painful to fix after launch. Launching a website is the moment a dozen small details suddenly matter all at once. This checklist walks through what to verify before you flip the switch, grouped so you can work through it top to bottom. Most items link to a full guide if you need the how. ## Server and access - [ ] Server provisioned with [Nginx](/how-to-install-nginx), [PHP](/how-to-install-php), and a [database](/how-to-install-mysql). - [ ] SSH hardened — key-only login, root login disabled, and ideally a [non-default SSH port](/change-ssh-port-ubuntu). - [ ] A firewall (`ufw`) allowing only the ports you actually use (80, 443, SSH). - [ ] [Swap space configured](/add-swap-space-on-ubuntu) so a memory spike doesn't kill processes. ## Domain and HTTPS - [ ] DNS records pointing at the server and propagated. - [ ] A valid SSL certificate installed — via [Certbot](/how-to-install-certbot) or [Cloudflare](/what-is-cloudflare). - [ ] [Automatic renewal](/renew-ssl-certificates-automatically) set up so the certificate never lapses. - [ ] HTTP redirects to HTTPS, and HSTS is enabled — aim for an [A+ SSL grade](/a-plus-grade-ssl-using-cloudflare). - [ ] No mixed content (no `http://` assets on an `https://` page). ## Security - [ ] [Debug mode **off** in production](/features/turn-off-debug-mode-automatically-on-production) — a leaked stack trace exposes paths, config, and sometimes credentials. - [ ] Security headers set (CSP, HSTS, `X-Content-Type-Options`) — verify with the [validation tools](/tools-to-validate-website). - [ ] Database user scoped to its own database, never connecting as root. - [ ] Secrets in [environment variables](/environment-variables-laravel), not in code or version control. - [ ] Review [web application security](/optimize-web-application-security) for the full picture. ## Performance - [ ] [Gzip or Brotli compression](/enable-gzip-compression-nginx) enabled. - [ ] [OPcache enabled](/enable-opcache-php) for PHP. - [ ] Static assets cached with sensible far-future headers. - [ ] A reasonable [time to first byte](/measure-ttfb) — see [optimizing website performance](/optimize-website-performance). ## Reliability - [ ] Automated [database backups](/backup-mysql-databases-single-file) running on a schedule — and test a restore. - [ ] [File backups](/features/safely-backup-files-and-databases) for anything users upload. - [ ] [Uptime and error monitoring](/features/monitor-your-sites-and-servers) so you hear about problems before your visitors do. - [ ] Custom error pages for [404, 500](/500-internal-server-error-laravel), and [502](/502-bad-gateway-nginx) instead of raw server defaults. ## After launch - [ ] Run the full suite of [website validation tools](/tools-to-validate-website). - [ ] Confirm email sending works and isn't landing in spam — see [email deliverability](/improving-email-deliverability). - [ ] Watch logs and metrics for the first day to catch anything the tests missed. ## Let Rocketeers handle it Look back over that list — a large share of it is server configuration and ongoing operations: TLS and renewal, security headers, compression, backups, monitoring, and keeping debug mode off in production. Rocketeers handles those by default when it provisions a server and deploys a site, and keeps watching afterwards, so your launch checklist is mostly green before you even start ticking boxes. ### What is Cloudflare? Source: https://rocketeersapp.com/what-is-cloudflare Cloudflare sits between your visitors and your server, acting as a CDN, DNS host, and security layer all at once. Here is what it actually does and how it fits in front of your site. [Cloudflare](/providers/cloudflare) is a service that sits in front of your website. Instead of visitors connecting straight to your server, their requests go to Cloudflare first, and Cloudflare decides what to serve from its own global network and what to forward to you. That one change — putting a smart layer between the internet and your origin server — is what makes everything else it offers possible. ## How it works: the proxy When you move your DNS to Cloudflare and switch a record to "proxied," its public address becomes a Cloudflare IP, not yours. Every request now lands on the nearest Cloudflare data center, which either answers from cache or passes it back to your origin server. Your real server address is hidden behind Cloudflare. That proxy position is the key idea. Because traffic flows through Cloudflare, it can cache content, filter attacks, and terminate HTTPS — all before a request ever reaches you. ## What it gives you ### DNS hosting Cloudflare is one of the fastest DNS providers, and it's free. You manage your records — A, CNAME, MX, TXT — in its dashboard, and changes propagate quickly. ### A CDN (content delivery network) Cloudflare caches your static files — images, CSS, JavaScript — in data centers around the world. A visitor in Tokyo is served from a nearby cache instead of waiting for a round trip to a server in Europe, which cuts load time and takes work off your origin. ### Free SSL/TLS Cloudflare provides a trusted certificate for the connection between the visitor and Cloudflare at no cost, so your site is HTTPS without you issuing a certificate. (You still want a certificate on your origin for the Cloudflare-to-server hop — see [getting an A+ SSL grade with Cloudflare](/a-plus-grade-ssl-using-cloudflare).) ### Security: DDoS protection and a WAF Because attacks hit Cloudflare's network first, it can absorb large denial-of-service floods and block malicious requests with its Web Application Firewall before they ever reach your server. ### Hiding your origin With the proxy on, your server's real IP isn't public, which makes it much harder for an attacker to target your machine directly. ## One thing to watch: real visitor IPs Once traffic is proxied, every request arrives from a Cloudflare IP, so your server logs would show Cloudflare instead of your actual visitors. The fix is to tell your web server to trust Cloudflare's IP ranges and read the real client address from the `CF-Connecting-IP` header. Cloudflare publishes its IP ranges, and they change over time, so this needs to stay current. ## Should you use it? For most sites the free tier is a clear win on speed and security. The trade-offs and the cases where it really pays off are covered in [why do you need Cloudflare](/why-do-you-need-cloudflare). ## Let Rocketeers handle it The fiddly part of Cloudflare isn't signing up — it's the wiring: importing your sites, getting the SSL mode right so you don't end up in a [redirect loop](/err-too-many-redirects), and keeping your web server's trusted-IP list in sync with Cloudflare's ranges so your logs and rate limits see real visitors. Rocketeers [imports your sites into Cloudflare](/features/import-multiple-sites-into-cloudflare) and keeps the IP ranges updated on your servers automatically, so the proxy works correctly without manual upkeep. ### Why do you need Cloudflare? Source: https://rocketeersapp.com/why-do-you-need-cloudflare You can run a website without Cloudflare — but here is what it gives you for free, when it genuinely matters, and the honest trade-offs. Strictly speaking, you don't *need* Cloudflare. Plenty of sites run fine without it. But for a free service it delivers an unusual amount, and there are situations where it turns a hard problem into a non-issue. Here's an honest look at why you'd want it — and when you might not. If you're new to what it actually is, start with [what is Cloudflare](/what-is-cloudflare). ## The case for using it ### It makes your site faster, globally Cloudflare caches your static assets in data centers worldwide, so visitors far from your server don't pay the full round-trip cost. If your audience is spread across regions, this is the cheapest way to improve [time to first byte](/measure-ttfb) and overall load time without renting servers in every continent. ### It absorbs attacks for you A denial-of-service flood hits Cloudflare's network, not your server. For a single-server setup, that's protection you simply cannot build yourself at any reasonable cost. The Web Application Firewall also blocks common exploit attempts before they reach your application. ### Free, trusted SSL You get HTTPS at the edge without issuing or renewing a certificate. That alone removes a whole class of work — though for the best security you still pair it with a certificate on your origin and aim for an [A+ SSL grade](/a-plus-grade-ssl-using-cloudflare). ### It hides your server With the proxy enabled, your origin IP isn't public. Attackers can't easily target a machine they can't find. ### Fast, free DNS Even if you used nothing else, Cloudflare is among the quickest DNS hosts available — and managing records is straightforward. ## When you might not need it - **Purely local or internal sites** that the public internet never reaches gain little from a global CDN. - **Apps that can't tolerate a shared proxy** — some real-time or non-HTTP workloads need direct connections. - **You already have a CDN and DDoS protection** from another provider and don't want a second layer. It's also worth remembering that proxying adds a hop in front of your origin. Misconfigure the SSL mode and you get an [ERR_TOO_MANY_REDIRECTS](/err-too-many-redirects) loop; forget to trust its IP ranges and your logs show Cloudflare instead of real visitors. The benefits are large, but it isn't zero-configuration. ## The bottom line For the vast majority of public websites, the free tier is close to a no-brainer: faster delivery, real attack protection, free SSL, and a hidden origin, all at no cost. The reason people hesitate isn't the value — it's the setup and upkeep. ## Let Rocketeers handle it The value of Cloudflare is obvious; the friction is in wiring it up correctly and keeping it that way. Rocketeers [imports your domains into Cloudflare](/features/import-multiple-sites-into-cloudflare), sets sane SSL and DNS defaults, and keeps your servers' trusted-IP lists in sync with Cloudflare's ranges — so you get the speed and protection without the configuration that usually comes with it. ### How to host your own website Source: https://rocketeersapp.com/how-to-host-your-own-website Hosting a website yourself means turning a bare server into a secure, fast, production-ready machine. Here is the full picture — every step, in order, with a guide for each one. "Just get a server and put your site on it" hides a surprising amount of work. A fresh Ubuntu box is a blank machine: no web server, no PHP, no database, no certificate, no firewall, no backups. Turning it into something you'd trust with real traffic is a sequence of steps, each with its own pitfalls. This is the whole map, in order, so you can see what hosting your own site actually involves — and follow a dedicated guide for each part. ## 1. Get and secure a server Start with a virtual server from a cloud provider — DigitalOcean, Hetzner, Vultr, AWS, or similar — running a current Ubuntu LTS. Before anything else, lock it down: - [Connect over SSH](/connect-to-server-ssh-command) using a key, and disable password login. - Disable root login and consider [changing the SSH port](/change-ssh-port-ubuntu). - Enable a firewall (`ufw`) that allows only SSH, HTTP, and HTTPS. - [Add swap space](/add-swap-space-on-ubuntu) so a memory spike doesn't take the box down. Skipping this step is how servers get compromised within hours of going online. ## 2. Install the web server [Nginx](/how-to-install-nginx) is the front door — it answers every request, serves static files, and hands dynamic requests to your application. You'll set up a server block per site and learn the [try_files directive](/nginx-try-files) that routes requests correctly. ## 3. Install PHP [Install PHP and PHP-FPM](/how-to-install-php) from the ondrej/php repository, with the extensions your application needs. If you host more than one site, you'll likely want [multiple PHP versions side by side](/how-to-install-multiple-php-versions-on-same-server). Then [tune php.ini](/important-php-config-options) for production — the defaults are not. ## 4. Install a database [Install MySQL](/how-to-install-mysql) (or PostgreSQL), secure it, and create a dedicated database and user for your app — never let it connect as root. You'll want to know how to [import](/import-database-mysql-command-line) and [export](/export-database-mysql-command-line) data, too. ## 5. Install Node.js Most modern sites compile their frontend assets, so [install Node.js](/how-to-install-nodejs) to run the build step during deployment. ## 6. Point your domain and add SSL - Set your DNS records to point at the server — [Cloudflare](/what-is-cloudflare) is a fast, free option that adds a CDN and DDoS protection on top. - Issue a trusted certificate with [Certbot](/how-to-install-certbot). - Set up [automatic renewal](/renew-ssl-certificates-automatically) so it never expires. - Tighten the configuration toward an [A+ SSL grade](/a-plus-grade-ssl-using-cloudflare). Without a valid certificate, every visitor sees [your connection is not private](/your-connection-is-not-private). ## 7. Deploy your code Now actually get your application onto the server. Doing it properly means [zero-downtime deployments](/zero-downtime-php-deployments): separate release folders, an atomic symlink swap, and the ability to roll back instantly — not overwriting live files in place. ## 8. Make it production-ready The work that separates a hobby setup from a real one: - **Performance** — [enable gzip/Brotli](/enable-gzip-compression-nginx) and [OPcache](/enable-opcache-php), and watch your [time to first byte](/measure-ttfb). - **Security** — set security headers, keep debug mode off, and review [application security](/optimize-web-application-security). - **Backups** — schedule [database backups](/backup-mysql-databases-single-file) and [test a restore](/features/safely-backup-files-and-databases). A backup you've never restored isn't a backup. - **[Monitoring](/features/monitor-your-sites-and-servers)** — uptime and error alerts so you hear about problems first. ## 9. Verify before you launch Run the [website validation tools](/tools-to-validate-website) and work through the [launch checklist](/website-checklist) so nothing slips through. ## So… should you do all this yourself? You absolutely can — and doing it once is the best way to understand how a server actually works. But notice what you've signed up for: not just the install, but the upkeep. Security patches, certificate renewals, PHP upgrades, backups you have to verify, and monitoring you have to watch — forever, for every site and every server. ## Let Rocketeers handle it Everything on this page — securing the server, installing and tuning Nginx, PHP, MySQL, and Node, issuing and renewing certificates, wiring up Cloudflare, deploying with zero downtime, backups, and monitoring — is exactly what Rocketeers does for you. You connect a server and a repository; it provisions the stack the production way and keeps it running. The point of understanding all these steps is knowing what you're getting back when you stop doing them by hand. ### No space left on device on Ubuntu Source: https://rocketeersapp.com/no-space-left-on-device-ubuntu This error means a filesystem is full, or out of inodes. Here is how to find what is eating the disk and reclaim it safely without breaking your server. ## About the error You'll see it from almost any program that tries to write: ```bash write error: No space left on device ``` It means the filesystem you're writing to is full. Occasionally the disk has free bytes but has run out of **inodes** (the structures that track files), which produces the same message. ## Find what's full Start with a summary of disk usage per filesystem: ```bash df -h ``` If a filesystem shows 100% but you can't see why, check inodes too: ```bash df -i ``` Then find the biggest directories on the full filesystem: ```bash sudo du -h --max-depth=1 / | sort -hr | head -20 ``` Drill down into whichever directory is largest by repeating `du` one level deeper. ## Common culprits and fixes ### Systemd journal logs The journal under `/var/log/journal` can quietly grow to gigabytes. Trim it: ```bash sudo journalctl --vacuum-time=7d # or cap by size sudo journalctl --vacuum-size=200M ``` Cap it permanently in `/etc/systemd/journald.conf`: ```ini SystemMaxUse=200M ``` ### Application and web server logs A runaway log file (nginx, Laravel, a stuck cron) is a classic cause. Truncate it in place rather than deleting it, so the writing process keeps its file handle: ```bash sudo truncate -s 0 /var/log/nginx/error.log ``` Set up `logrotate` so it can't happen again. ### Package cache On a server that's been updated a lot, the apt cache reclaims gigabytes: ```bash sudo apt clean sudo apt autoremove --purge ``` ### Old kernels and Docker Old kernels and unused Docker images/volumes are frequent space hogs: ```bash sudo apt autoremove --purge # removes old kernels docker system prune -a # if you run Docker ``` For more cleanup ideas see [reclaiming disk space on Ubuntu](/reclaim-diskspace-on-ubuntu). If the disk filled because of swap or you have none, see [adding swap space on Ubuntu](/add-swap-space-on-ubuntu). ### Address already in use (port already bound) Source: https://rocketeersapp.com/address-already-in-use-port This error means another process is already listening on the port you tried to bind. Find what is using it and either stop that process or pick a different port. ## About the error Starting a server fails with one of these: ```bash bind() to 0.0.0.0:80 failed (98: Address already in use) # nginx Error: listen EADDRINUSE: address already in use :::3000 # Node [ERROR] Can't start server: Bind on TCP/IP port: Address already in use # MySQL ``` Only one process can listen on a given port at a time. The bind fails because something already holds it. ## Why do I see this error - The service is **already running** (you started it twice). - A previous instance crashed but didn't release the port yet. - A **different** program is using that port (Apache holding 80 when you start nginx, another dev server on 3000). - A `php artisan serve` or Vite process from an earlier session is still alive. ## Solution ### Find what's using the port `ss` (or `lsof`) shows the process holding the port. For port 80: ```bash sudo ss -tlnp | grep ':80' # or sudo lsof -i :80 ``` The output includes the PID and program name, exactly what's holding it. ### Stop it or kill it If it's a service you control, stop it properly: ```bash sudo systemctl stop apache2 # e.g. Apache squatting on port 80 ``` If it's a stray process that won't go away, kill it by PID: ```bash kill # if it ignores that: kill -9 ``` To kill whatever is on a port in one step: ```bash sudo fuser -k 80/tcp ``` ### Or use a different port If you actually want both running, change the port of the one you're starting. For `artisan serve`: ```bash php artisan serve --port=8001 ``` ### Crashed process, port still held A port can stay in `TIME_WAIT` briefly after a crash. Wait a few seconds and retry, or confirm with `ss` that nothing still owns it before restarting. For the focused walkthrough, see [how to kill the process on a port](/kill-process-on-port-linux). ### SSH Permission denied (publickey) Source: https://rocketeersapp.com/ssh-permission-denied-publickey This SSH error means the server rejected every key your client offered. Usually the right key is not being sent, or the file permissions on the server are wrong. ## About the error Connecting over SSH fails with: ```bash Permission denied (publickey). ``` The server only accepts public-key authentication, and none of the keys your client presented matched an entry in the account's `authorized_keys`. A related variant, `Too many authentication failures`, means your client offered so many wrong keys that the server cut you off before reaching the right one. ## Why do I see this error - Your public key isn't in the server's `~/.ssh/authorized_keys`. - The client is offering the wrong key, or every key in your agent. - File permissions on the server's `.ssh` directory or `authorized_keys` are too open, so sshd ignores them. - You're connecting as the wrong user. ## Solution ### Diagnose first Verbose output shows exactly which keys are offered and how the server responds: ```bash ssh -vvv user@your-server ``` ### Make sure your key is on the server The easiest way to install your public key is `ssh-copy-id`: ```bash ssh-copy-id user@your-server ``` Or append it manually to `~/.ssh/authorized_keys` on the server. ### Offer only the right key If you have many keys, the agent offers them all and can trip "Too many authentication failures". Force a single key and ignore the agent: ```bash ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 user@your-server ``` Make it permanent in `~/.ssh/config`: ``` Host your-server HostName your-server.example.com User deployer IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes ``` ### Fix permissions on the server sshd refuses keys if the files are world-writable. The `.ssh` directory must be `700` and `authorized_keys` must be `600`, both owned by the account's user: ```bash chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys chown -R $USER:$USER ~/.ssh ``` After fixing this, reconnect and the key should be accepted. ### How many CPU cores on Ubuntu Source: https://rocketeersapp.com/how-many-cpu-cores-on-ubuntu This command for Ubuntu shows how many CPU cores are available on your machine. Simple little command to get all the CPU cores active on your machine running on Ubuntu: ```bash nproc --all ``` That's it! To check available memory too, see [how much RAM memory is on your server](/how-much-memory-on-ubuntu), and [which processes use the most CPU](/top-processes-cpu). ### How much RAM memory on Ubuntu Source: https://rocketeersapp.com/how-much-memory-on-ubuntu This command for Ubuntu shows you the RAM memory on your server The easiest way to see all installed, used, free and available RAM memory on your Ubuntu server is using the `free` command, we use the `-h` parameter for human readable sizes: ```bash free -h ``` This outputs for example: ``` total used free shared buff/cache available Mem: 7.8Gi 4.9Gi 609Mi 7.0Mi 2.3Gi 2.6Gi Swap: 2.0Gi 253Mi 1.8Gi ``` If you only want one of these specific numbers: ```bash # Total memory (ex. 7.8Gi) free -h | awk '/^Mem:/ {print $2}' # Used memory (ex. 4.9Gi) free -h | awk '/^Mem:/ {print $3}' # Free memory (ex. 609Mi) free -h | awk '/^Mem:/ {print $4}' # Shared memory (ex. 7.0Mi) free -h | awk '/^Mem:/ {print $5}' # Buffers/cache memory (ex. 2.3Gi) free -h | awk '/^Mem:/ {print $6}' # Available memory (ex. 2.6Gi) free -h | awk '/^Mem:/ {print $7}' ``` To count CPU cores too, see [how many CPU cores your server has](/how-many-cpu-cores-on-ubuntu). When memory runs low, find [the processes using the most](/top-processes-memory) or [add swap space](/add-swap-space-on-ubuntu). ### Zero downtime deployments using PHP-FPM and nginx Source: https://rocketeersapp.com/zero-downtime-php-deployments Let me show you how you can deploy your PHP website or web applications without any downtime, using separate releases so you can rollback to a previous release quickly. ## Deploying with zero downtime Of course you want to deploy your PHP applications without any interruptions for your visitors and users. But somehow, this isn't as simple as it reads. The combination of PHP (FPM), nginx and separate release folders make this task a little more complex that we would like. That's the bad news, the good news is we have a great way to solve these issues. ## Why separate releases are important Why is it important to have separate releases? While deploying a new release, you still want to have the current release up and running. This can only be achieved by not overwriting the old release with the new release. Otherwise your active release will have downtime while updating and replacing the files from your fresh deploymnet. This is also the only way we can make sure you can always instantly rollback to a previous deployment, when it's clear that there are problems with the just released version. By using a symlink, we can achieve multiple releases and point to one specific release. ## Common issues The most common issue regarding zero downtime deployments is the moment of switching from the current to the next version, this could require reloading the nginx and PHP FPM processes and so you get challenged to keep your uptime. So reloading these services should be avoided and that is the tricky part. But we've figured the ultimate way to omit reloading and to keep your application and active users safe! ## The deployment process ### Creating a release structure First we need to create a top folder `/releases` where all releases can be found and a new release using a date format. I like to format `Y-m-d-HMS` which translates to `Year-Month-Day-HourMinuteSeconds` and for example `2024-10-08-144122`. This makes sure you can always distinguish a release folder from the time it was created and it orders them from old to new automatically. Also we state that we have an active release folder, that's in `/releases/current`. Where `current` is a symlink (`ln -s`) to the folder with the currently active release. ```bash NEW_RELEASE_DIRECTORY=$(date +"%Y-%m-%d-%H%M%S") # Create `releases` folder mkdir -p /releases # Create folder for the new release mkdir $NEW_RELEASE_DIRECTORY ``` We put the new release directory inside a variable because we need the same directory multiple times in the next steps, and we don't want to have a new timestamp every time we need it. ### Pulling in the code Then we clone the code from the git repository. In this step it's important to not pull in anymore then we need for a new release. This means no additional history, because this will only take in extra space and makes the downloading of all data take longer and therefore slows down your deployment time. ```bash git clone \ --depth 1 \ --branch main \ --single-branch \ git@github.com:rocketeers-app/rocketeers.git \ $NEW_RELEASE_DIRECTORY ``` ### Install the application This step contains al the commands you need to install your application. Using [Laravel](https://laravel.com) this will mean installing dependencies using Composer and possibly npm but also running migrations and clearing caches. Here is an example Laravel deployment script for a Filament app: ```bash # Install Composer dependencies php composer install \ --no-ansi \ --no-dev \ --no-interaction \ --no-progress \ --optimize-autoloader \ --prefer-dist # Run database migrations php artisan migrate # Terminate Horizon php artisan horizon:terminate # Clear caches php artisan cache:clear # Clear expired password reset tokens php artisan auth:clear-resets # Cache views php artisan view:cache # Cache routes php artisan route:cache # Link new storage folder php artisan storage:link # Optimize filament assets php artisan filament:assets # Cache Filament components and icons php artisan filament:optimize # Install npm dependencies npm install # Build assets using Vite npx vite build ``` ### Activate the new release Now the critically and most significant step of the deployment process: activating the new release WITHOUT causing downtime! First we enter the releases folder: ```bash cd /releases ``` Then we create a new symlink called `deployment` inside: ```bash # Create deployment symlink ln -s ./releases/$NEW_RELEASE_DIRECTORY deployment ``` Now, to switch from the previous release (dynamically pointed by `releases/current`) to the new release folder, we overwrite the active `current` symlink with the newly created `deployment` symlink: ```bash # Swap symlinks by overwriting the old with the new symlink mv -Tf deployment current ``` And this makes all the difference, because this creates and overwrites a new symlink PHP-FPM and nginx will detect the change without the need of reloading! ### After deployment tasks Sometimes it's needed to execute some optimization tasks, on a server with not a lot of diskspace, you could remove all `.git` folders to create some space: ```bash # Remove .git folders from root and vendor files rm -Rf ./.git find vendor -type d -name '.git' -exec rm -rf {} + ``` ### That's it! Now we have successfully deployed a PHP application without reloading anything and with zero downtime. Rocketeers runs exactly this as [zero-downtime atomic deployments](/features/zero-downtime-atomic-deployments) for every site you host. ### How to check Ubuntu version Source: https://rocketeersapp.com/ubuntu-version Learn how to quickly get the Ubuntu version you're running using the command line. If you want to know which Ubuntu version your server is running, simply execute the following command using the command line: ```bash lsb_release -a ``` If you want to get a more specific output, so you only get one of the lines output by the command above. You can use the following parameters, where you'll see that every line has a logical letter corresponding to the label name: ```bash lsb_release -i # Outputs: "Distributor ID: Ubuntu" lsb_release -d # Outputs: "Description: Ubuntu 18.04.6 LTS" lsb_release -r # Outputs: "Release: 18.04" lsb_release -c # Outputs: "Codename: bionic" ``` If you only want the value and not the label of the output, add the `s` parameter to the command: ```bash lsb_release -is # Outputs: "Ubuntu" lsb_release -ds # Outputs: "Ubuntu 18.04.6 LTS" lsb_release -rs # Outputs: "18.04" lsb_release -cs # Outputs: "bionic" ``` For more ways to check, see [checking your Ubuntu version from the command line](/check-ubuntu-version-command-line), then [update Ubuntu](/update-ubuntu-command-line) to pull in security patches. ### Disable unnecessary and unused PHP versions (FPM pools) Source: https://rocketeersapp.com/disable-unused-php-fpm-pools Hosting your application optimally means you need to take care of the valuable server resources. PHP FPM can keep memory occupied even when not actively used. When running PHP applications it's a common mistake to forget to turn off unused PHP versions. This mostly happens when you install a new PHP version and forget to uninstall or disable the previous PHP versions. It is no problem to keep multiple PHP versions installed on your server. But it could be a problem to keep it running on your server. Likely this happens when using PHP FPM, which has a default www pool per PHP version that isn't turned off when it's not in use anymore. So it could be running forerver and keeps precious server memory occupied. ## Stop unused PHP versions (FPM pools) Stopping the processes for a PHP FPM pool is easy, using this command for your unneeded PHP version (e.g. 7.4): ```bash sudo service php7.4-fpm stop ``` ## Prevent PHP versions from starting on boot To prevent the PHP version from starting when you reboot your server, disable the service all together: ```bash sudo systemctl disable php7.4-fpm ``` > Using [Rocketeers](/) you won't have this problem. When changing PHP versions, Rocketeers will detect which sites and applications are running which versions and will disable all unused versions on your server. These pools come from [installing multiple PHP versions](/how-to-install-multiple-php-versions-on-same-server); see [how to install PHP](/how-to-install-php) for the base setup. ### Reclaim disk space on Ubuntu server Source: https://rocketeersapp.com/reclaim-diskspace-on-ubuntu Your server disk space can clog up very quickly with a lot of unneeded files. Let's find out how to reclaim your precious disk space back. There are a few ways that make it possible to reclaim diskspace right away on a server. This is very handy when your server is low on diskspace. Let's dive in! ## Shrink log files Using this command, we can shrink logfiles within a specific size. In this example we shrink all system log files to have a maximum size of 500 MB: ```bash sudo journalctl --vacuum-size=500M ``` **This article will be updated with more examples in the future** If the disk is already full, start with [no space left on device](/no-space-left-on-device-ubuntu). On Docker hosts, [prune unused Docker data](/docker-prune) to claw back space. ### How to add Swap Space on Ubuntu servers Source: https://rocketeersapp.com/add-swap-space-on-ubuntu Servers with little resources can benefit greatly from adding some swap space for memory usage.3333 On servers with not a lot of [RAM memory](/how-much-memory-on-ubuntu) (< 1 GB) it's recommended to add some extra swap space in case a server needs a little bit more memory to keep everything going and to prevent it runs out of memory and can't continue processing requests. ## Choosing how much memory The first step is to decide how much swap space you want to add to the server. I choose mostly 1 GB or 2GB. In this case, the notiation should be 1G (GB) or 1000M (MB). ```bash sudo fallocate -l 1G /var/swap.1 ``` ## Enable swap space Next step is to setup the swap file correctly and activate the swap space: ```bash sudo chmod 600 /var/swap.1 sudo /sbin/mkswap /var/swap.1 sudo /sbin/swapon /var/swap.1 ``` ## Configure swappiness and cache pressure To configure how fast the system will make use of the swap memory, we can tweak the values of the swappiness and the cache pressure. The higher the swappiness, the faster it will reach for swap memory. Because we're running servers we would like to decrease the default swappiness (which is 60) of our Ubuntu servers. Cache pressure defines how fast the system will take the memory back after it has been used. After some research and testing we found that these values work well on most server configurations: ```bash sudo sysctl -w vm.swappiness=10 echo "vm.swappiness=10" | sudo tee -a /etc/sysctl.conf sudo sysctl -w vm.vfs_cache_pressure=50 echo "vm.vfs_cache_pressure=50" | sudo tee -a /etc/sysctl.conf ``` ### Make the changes permanent We want to make these changes permanent and keep them also after a server reboots, so we need to execute these commands to make sure they will: ``` echo "/var/swap.1 none swap sw 0 0" | sudo tee -a /etc/fstab sysctl -p ``` ### How to get top processes with highest memory usage Source: https://rocketeersapp.com/top-processes-memory This command shows you which processes are eating all of your server memory. Run this command to show the top 10 processes that are using the most memory (RAM) on your server: ```bash ps -eo cmd,%mem --sort=-%mem | head -n 11 ``` For CPU rather than memory, see [top processes by CPU](/top-processes-cpu). To check total available memory first, see [how much RAM your server has](/how-much-memory-on-ubuntu), and consider [adding swap space](/add-swap-space-on-ubuntu) if it's tight. ### How to get top processes with highest CPU usage Source: https://rocketeersapp.com/top-processes-cpu Want to know what's causing your server to slow down or what's keeping all these resources for itself. This command shows you which processes take it all. Run this commando to show top 10 processes that are using the most CPU on your server: ```bash ps -eo cmd,%cpu --sort=-%cpu | head -n 11 ``` To find memory hogs instead, use [top processes by memory](/top-processes-memory); and when a process is holding a port, here's [how to kill it](/kill-process-on-port-linux). ## Laravel ### Setting process priority for Laravel Horizon workers Source: https://rocketeersapp.com/laravel-horizon-nice-process-priority Laravel Horizon lets you set the Linux nice value for worker processes, giving them more or less CPU scheduling priority. ## Why you'd want this Linux schedules CPU time using a **nice value** between `-20` (highest priority) and `19` (lowest). By default processes start at `0`. If your server is dedicated to queue processing or you're handling time-sensitive, CPU-intensive jobs, lowering the nice value gives workers a bigger slice of CPU time. On a shared server running web traffic alongside workers, you'd leave it at `0` or increase it to avoid starving your web processes. ## How to configure it Add a `nice` key to the supervisor block in `config/horizon.php`: ```php 'environments' => [ 'production' => [ 'supervisor-1' => [ 'connection' => 'redis', 'queue' => ['default'], 'balance' => 'auto', 'processes' => 20, 'nice' => -5, ], ], ], ``` Horizon calls PHP's `proc_nice()` when it starts each worker. ## Permissions Raising the nice value (e.g. `0 → 10`) works for any user. Lowering it (e.g. `0 → -5`) requires either root or the `CAP_SYS_NICE` capability. Without those privileges Horizon throws: ``` proc_nice(): Operation not permitted ``` To grant the capability to PHP without running as root: ```bash sudo setcap cap_sys_nice=eip /usr/bin/php8.4 ``` Verify it was applied: ```bash getcap /usr/bin/php8.4 # /usr/bin/php8.4 cap_sys_nice=eip ``` Note: this grants the capability to the binary itself, so any PHP script running under that binary can adjust process priorities. ## Apply the changes ```bash php artisan config:clear php artisan horizon:terminate ``` If Horizon is managed by Supervisor: ```bash sudo supervisorctl restart horizon:* ``` ## Reverting Remove the capability from the PHP binary: ```bash sudo setcap -r /usr/bin/php8.4 ``` No output from `getcap` afterwards means no capabilities are set. Also set `'nice' => 0` in `config/horizon.php` (or remove the key entirely) and restart Horizon. For the broader picture of getting queues and the rest of the stack fast, see [Laravel performance](/laravel-performance). Horizon needs Redis — if workers can't connect, see [Redis connection refused](/redis-connection-refused-laravel). ### Redis connection refused in Laravel Source: https://rocketeersapp.com/redis-connection-refused-laravel When Laravel uses Redis for cache, sessions or queues, a "Connection refused" error means it cannot reach the Redis server. Usually the service is down or the host is wrong, especially in Docker. ## About the error ```bash Predis\Connection\ConnectionException: Connection refused [tcp://127.0.0.1:6379] ``` (Or the phpredis equivalent.) Laravel tried to open a connection to Redis and nothing accepted it at that address. This is different from an authentication or wrong-database error, the connection never got established at all. ## Why do I see this error - Redis isn't running. - `REDIS_HOST` or `REDIS_PORT` is wrong. - **In Docker**, `127.0.0.1` from inside the app container points at the container itself, not the Redis container. - Redis requires a password (`requirepass`) that you haven't set in `.env`. ## Solution ### Check Redis is running ```bash systemctl status redis-server redis-cli ping # should reply PONG ``` If `ping` replies `PONG`, the server is up and the problem is how Laravel addresses it. ### Verify the connection settings ```ini REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_PASSWORD=null CACHE_STORE=redis SESSION_DRIVER=redis QUEUE_CONNECTION=redis ``` ### Docker: use the service name Inside a container, `127.0.0.1` is the container, not the host. Set `REDIS_HOST` to the **service name** from your `docker-compose.yml`: ```ini REDIS_HOST=redis ``` ```yaml services: redis: image: redis:7 ports: - "6379:6379" ``` ### Clear cached config If you changed `.env` but Laravel still connects to the old address, the config is cached: ```bash php artisan config:clear ``` See [environment variables in Laravel](/environment-variables-laravel) for how these values are loaded, and [SQLSTATE[HY000] [2002] Connection refused](/sqlstate-hy000-2002-connection-refused) for the same class of "can't reach the service" problem with MySQL. ### Laravel failed to open stream: Permission denied Source: https://rocketeersapp.com/laravel-failed-to-open-stream-permission-denied A blank page or 500 error right after deploying is almost always a permission problem on the storage or bootstrap/cache directory. Here is how to fix the ownership properly. ## About the error You'll find a line like this in your log, or on screen with debug enabled: ```bash file_put_contents(/var/www/storage/logs/laravel.log): Failed to open stream: Permission denied ``` The path varies, it might be `storage/framework/views`, `storage/framework/cache`, or `bootstrap/cache`, but the cause is always the same: PHP tried to write a file and the operating system said no. ## Why do I see this error Laravel needs to write to a few directories while it runs: logs, compiled Blade views, the framework cache and the config/route cache. If the user running PHP-FPM (usually `www-data`) does not own those directories, every write fails. This bites most often right after a deploy or a `git clone`, because the files were created by your own user (or by root), not by the web server user. The daily log channel is a common trigger: whichever process writes the first log line of the day owns that file, and the other process then can't append to it. ## Solution Give ownership of the writable directories to the web server user and set sensible permissions: ```bash sudo chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache sudo chmod -R 775 /var/www/storage /var/www/bootstrap/cache ``` Replace `www-data` with `nginx` on RHEL/CentOS based systems, and adjust the path to your project root. If your deploy user and the web server user are different, add your deploy user to the web server group so both can write: ```bash sudo usermod -aG www-data deployer ``` After fixing permissions, clear any half-written cache files: ```bash php artisan optimize:clear ``` See [clearing the cache in Laravel](/clear-cache-laravel) for what that command does. ### A few warnings - **Never use `chmod 777`.** It lets any user on the server write to your application files, which is a real security risk. `775` with the correct group ownership is enough. - On **RHEL, CentOS or Fedora with SELinux** in enforcing mode, correct Unix permissions still aren't enough. You also need the right SELinux context: ```bash sudo chcon -R -t httpd_sys_rw_content_t /var/www/storage /var/www/bootstrap/cache ``` With debug off, this error reaches visitors as a generic [500 Internal Server Error](/500-internal-server-error-laravel). The same kind of permission problem on the web root itself (rather than `storage`) instead produces a [403 Forbidden in nginx](/403-forbidden-nginx). ### No application encryption key has been specified Source: https://rocketeersapp.com/no-application-encryption-key-has-been-specified This Laravel error appears when the APP_KEY is missing. It is one of the first things you hit after cloning a project. The fix is a single artisan command. ## About the error The full message is: ```bash RuntimeException: No application encryption key has been specified. ``` Laravel uses the `APP_KEY` to encrypt cookies, sessions and anything you pass through the `Crypt` facade. Without it, the framework refuses to boot rather than fall back to no encryption. ## Why do I see this error The `APP_KEY` value in your `.env` is empty or missing. This almost always happens right after: - Cloning a project from Git, the `.env` is gitignored, so you start without a key. - Copying `.env.example` to `.env` without generating a fresh key. ## Solution Generate a key. Laravel writes it straight into your `.env` for you: ```bash php artisan key:generate ``` If you don't have a `.env` file yet, create one first: ```bash cp .env.example .env php artisan key:generate ``` After generating, your `.env` will contain a value like: ```ini APP_KEY=base64:Rk9wq3y...= ``` With debug off, a missing key reaches visitors as a generic [500 Internal Server Error](/500-internal-server-error-laravel), so this is worth checking first on a fresh deploy. If you cached your config, clear it so the new key is picked up: ```bash php artisan config:clear ``` ### On a server If you see this in production, the deploy likely doesn't have an `APP_KEY` set. Generate one **once** and keep it stable, changing it later invalidates every existing session and encrypted cookie, logging all your users out and making old encrypted data unreadable. For a non-interactive deploy you can write the key without the confirmation prompt: ```bash php artisan key:generate --force ``` See [environment variables in Laravel](/environment-variables-laravel) for more on how `.env` values are loaded. ### MySQL 1071: Specified key was too long (Laravel migration) Source: https://rocketeersapp.com/mysql-1071-specified-key-was-too-long This migration error happens on older MySQL with the utf8mb4 charset, where indexed string columns exceed the index size limit. The fix is a one-line default in your service provider. ## About the error Running `php artisan migrate` on a fresh install against older MySQL throws: ```bash SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes ``` It almost always fails on the `users` table when creating a unique index on the email or another `VARCHAR(255)` column. ## Why do I see this error Laravel defaults to the `utf8mb4` charset (proper 4-byte Unicode, needed for emoji and many scripts). In `utf8mb4` each character can take up to 4 bytes, so a `VARCHAR(255)` index needs `255 × 4 = 1020` bytes. On **MySQL before 5.7.7** (and MariaDB before 10.2), the per-index limit without large-prefix support is **767 bytes**, less than 1020, so the index creation fails. ## Solution ### Recommended: set a default string length Laravel ships with a one-line fix. In `app/Providers/AppServiceProvider.php`, cap the default string length so indexed `VARCHAR` columns fit: ```php use Illuminate\Support\Facades\Schema; public function boot(): void { Schema::defaultStringLength(191); } ``` `191 × 4 = 764` bytes, just under the 767 limit. Then re-run the migration: ```bash php artisan migrate:fresh ``` ### Better long-term: upgrade MySQL The 767-byte limit is a limitation of old versions. MySQL 5.7.7+ and 8.0 raise it to 3072 bytes with the InnoDB large prefix, and the error disappears without capping your column lengths. See [upgrading MySQL 5.7 to 8.0 on Ubuntu](/upgrade-mysql-5-7-to-8-0-ubuntu). ### Per-column alternative If you only need it on a specific migration, set the length on the column directly instead of globally: ```php $table->string('email', 191)->unique(); ``` ### Target class does not exist in Laravel Source: https://rocketeersapp.com/target-class-does-not-exist-laravel This error means Laravel cannot resolve a controller or class you referenced in a route or container binding. Almost always a namespace, typo or autoload issue. ## About the error The message looks like: ```bash Illuminate\Contracts\Container\BindingResolutionException: Target class [App\Http\Controllers\UserController] does not exist. ``` Laravel's service container tried to instantiate a class by its name and couldn't find it. ## Why do I see this error There are a few usual suspects: - A typo in the controller name in your route definition. - A missing or wrong `namespace` at the top of the controller file. - You're using the **string** controller syntax on Laravel 8+, where the default namespace prefix was removed. - The autoloader hasn't picked up a newly created class. ## Solution ### Use the class-based route syntax Since Laravel 8 the recommended way to reference a controller is by importing it and using its `::class` constant, not a string: ```php use App\Http\Controllers\UserController; Route::get('/users', [UserController::class, 'index']); ``` This catches typos at compile time and resolves the namespace for you. The old string form relied on a namespace prefix that no longer exists by default: ```php // Fragile, the App\Http\Controllers prefix is no longer applied automatically Route::get('/users', 'UserController@index'); ``` ### Check the namespace Open the controller and confirm its namespace matches its folder: ```php namespace App\Http\Controllers; class UserController extends Controller { // ... } ``` A file in `app/Http/Controllers/Admin/` must declare `namespace App\Http\Controllers\Admin;`. ### Refresh the autoloader If the class genuinely exists and the namespace is right, regenerate Composer's autoload map and clear caches: ```bash composer dump-autoload php artisan optimize:clear ``` See [clearing the cache in Laravel](/clear-cache-laravel) for what gets cleared. A similar "it built but Laravel can't find it" error is [Vite manifest not found](/vite-manifest-not-found-laravel). ### Vite manifest not found in Laravel Source: https://rocketeersapp.com/vite-manifest-not-found-laravel This error means Laravel cannot find your compiled frontend assets. Either the dev server is not running, or you never built your assets for production. ## About the error The message reads: ```bash Illuminate\Foundation\ViteException: Vite manifest not found at: /var/www/public/build/manifest.json ``` The `@vite` directive in your Blade layout looks up that `manifest.json` to know which compiled CSS and JS files to load. If the file isn't there, Laravel throws. ## Why do I see this error The manifest is generated by Vite when it builds your assets. It's missing because: - You're in development but the Vite dev server isn't running. - You're in production but you never ran the build step. - The `public/build` directory wasn't deployed to the server. ## Solution ### In development Start the Vite dev server and leave it running alongside your application: ```bash npm run dev ``` While `npm run dev` is running, `@vite` talks to the dev server directly and no manifest file is needed. ### In production Build the assets. This is what generates `public/build/manifest.json`: ```bash npm install npm run build ``` Make this part of your deploy process so it runs on every release. If you build locally and deploy, make sure the `public/build` directory is actually uploaded and **not** excluded by a `.gitignore` or rsync filter. ### Older projects (Laravel Mix) If your project still uses Laravel Mix instead of Vite, the equivalent error mentions `mix-manifest.json`. The fix is the same idea with Mix's commands: ```bash npm run dev # development npm run production # build for production ``` For zero-downtime deploys where assets are built per release, see [zero downtime PHP deployments](/zero-downtime-php-deployments). ### 500 Internal Server Error in Laravel Source: https://rocketeersapp.com/500-internal-server-error-laravel A 500 error is a generic "something broke" on the server. The error itself tells you nothing, the real message is in your logs. Here is where to look and the usual culprits. ## About error 500 A `500 Internal Server Error` means the server hit an unhandled error while processing the request. It's deliberately generic: in production Laravel hides the details so it doesn't leak sensitive information to visitors. The actual cause is always written somewhere you can read. ## Where to look first Don't guess, read the log. For Laravel: ```bash tail -n 100 storage/logs/laravel.log ``` If nothing's there, the error happened before Laravel booted, so check the web server and PHP-FPM logs: ```bash tail -n 100 /var/log/nginx/error.log tail -n 100 /var/log/php8.3-fpm.log ``` See [logging in Laravel](/laravel-logging) for configuring where these go. ## Common causes - **Missing `APP_KEY`**, see [No application encryption key has been specified](/no-application-encryption-key-has-been-specified). - **File permissions** on `storage` or `bootstrap/cache`, see [failed to open stream: Permission denied](/laravel-failed-to-open-stream-permission-denied). - **A stale cached config** referencing values that no longer exist. - **A missing `.env`** or wrong database credentials. - **A PHP error**, out of memory, or a fatal syntax error in deployed code. ## Solution ### Temporarily see the real error On a staging or local environment, enable debug mode to render the actual exception instead of the generic page: ```ini APP_DEBUG=true ``` **Never leave `APP_DEBUG=true` on in production**, it exposes stack traces, environment values and database details to the public. Turn it back off the moment you've found the cause. ### Clear stale caches A frequent cause right after deploy is a cached config or view pointing at something stale: ```bash php artisan optimize:clear ``` This clears the config, route, view and event caches in one go, see [clearing the cache in Laravel](/clear-cache-laravel). ### Re-cache for production Once it works, rebuild the caches for performance: ```bash php artisan config:cache php artisan route:cache ``` ### CSRF token mismatch in Laravel Source: https://rocketeersapp.com/csrf-token-mismatch-laravel A CSRF token mismatch means Laravel rejected a request because its token was missing, wrong, or expired. It is the same protection behind the 419 page, here is how to send the token correctly. ## About the error You'll see `CSRF token mismatch.` in an exception or API response, or the user lands on a [419 Page Expired](/419-page-expired-laravel) page. Laravel verifies a token on every state-changing request (`POST`, `PUT`, `PATCH`, `DELETE`) to block Cross-Site Request Forgery, one of several [web application security](/optimize-web-application-security) protections. If the token doesn't match the one in the session, the request is rejected. This is unrelated to a [CORS error](/cors-error-no-access-control-allow-origin), a separate browser-enforced cross-origin check that's easy to confuse with it. ## Why do I see this error - A form was submitted **without the `@csrf` token**. - An **AJAX request** didn't send the `X-CSRF-TOKEN` header. - The session **expired** (the page sat open too long), so the token is stale. - A **session/cookie problem**: wrong `SESSION_DOMAIN`, or cookies blocked behind a proxy. ## Solution ### Forms Add the `@csrf` Blade directive inside every form. It outputs the hidden `_token` field Laravel checks: ```blade
@csrf
``` ### AJAX requests Expose the token in a meta tag and send it as a header on every request. With Axios: ```html ``` ```javascript window.axios.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').content; ``` With jQuery: ```javascript $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); ``` ### Expired sessions If users hit this after leaving a tab open, the token expired with the session. You can't avoid expiry entirely, but you can detect a 419 in your AJAX layer and refresh the page or token gracefully rather than failing silently. ### Stateless routes (APIs, webhooks) CSRF protection is for session-based, browser-driven requests. For a stateless API or an incoming webhook it doesn't apply, exclude those routes and authenticate with tokens or signed URLs instead. See [disabling CSRF in Laravel](/disable-csrf-in-laravel) for how to exclude specific routes. ### Environment variables in Laravel Source: https://rocketeersapp.com/environment-variables-laravel A complete list of all available environment variables used in the latest Laravel (version 11). ## What is an environment variable Laravel has a `config` folder that contains all configuration files in one place, to change a configuration you could change the value in the code, or change it inside the `.env` when it has an environment variable as the default value (with a fallback). Using the `.env` file has the advantage that every environment (typically local, staging or production) can have its own configuration based on the (yes) environment it runs in. The advantage of using the config files without the `env()` helper has the advantage that other developers on the same project can't forget to set important configurations to certain required values. ## Typically used for secrets But beware, everything that's a secret, should only be present in the `.env` file and never in your code or version control (like git). ## Don't use `env()` outside the config folder It's considered a bad practice to use the `env()` helper outside your `config` folder. Always use the `config()` helper instead. Why? Because Laravel has a feature called config caching (using `artisan config:cache`) and when the cache is created, Laravel will not read the `.env` file anymore because it solely relies on the cached configuration inside your `config` folder. ## Keep your `APP_KEY` safe When using encryption, Laravel relies on the `APP_KEY` environment variable. This variable can be conveniently generated using `artisan key:generate`. But be careful, already encrypted values rely on the value of this key. When the key is changed, the current encrypted values cannot be decrypted anymore. If you need to change the `APP_KEY`, make sure you'll make use of [gracefully rotating the encryption keys](https://laravel.com/docs/11.x/encryption#gracefully-rotating-encryption-keys). Save the `APP_KEY` somewhere safe, when it's gone, your encrypted values in for example your database are also gone. > A common misconception is that the passwords in your user table are also relying on the `APP_KEY`. This is not the case! > > Passwords are hashed values and are never meant to be un-hashed. Validating a password relies on the hashing algorithm (for Laravel bcrypt is the default driver) and the `APP_KEY` is only used for encrypting and decrypting. To change your Laravel app's configuration per environment, you can use the `.env` file (derived from `.env.example`) included in every Laravel project. But this does not list all possible `env` variables. So here's a complete list with all default environment variables. ## List of all environment variables ```bash APP_DEBUG APP_ENV APP_FAKER_LOCALE APP_FALLBACK_LOCALE APP_KEY APP_LOCALE APP_MAINTENANCE_DRIVER APP_MAINTENANCE_STORE APP_NAME APP_PREVIOUS_KEYS APP_TIMEZONE APP_URL AUTH_GUARD AUTH_MODEL AUTH_PASSWORD_BROKER AUTH_PASSWORD_RESET_TOKEN_TABLE AUTH_PASSWORD_TIMEOUT AWS_ACCESS_KEY_ID AWS_BUCKET AWS_DEFAULT_REGION AWS_ENDPOINT AWS_SECRET_ACCESS_KEY AWS_URL AWS_USE_PATH_STYLE_ENDPOINT BEANSTALKD_QUEUE BEANSTALKD_QUEUE_HOST BEANSTALKD_QUEUE_RETRY_AFTER CACHE_STORE DB_CACHE_CONNECTION DB_CACHE_LOCK_CONNECTION DB_CACHE_TABLE DB_CHARSET DB_COLLATION DB_CONNECTION DB_DATABASE DB_ENCRYPT DB_FOREIGN_KEYS DB_HOST DB_PASSWORD DB_PORT DB_QUEUE DB_QUEUE_CONNECTION DB_QUEUE_RETRY_AFTER DB_QUEUE_TABLE DB_SOCKET DB_TRUST_SERVER_CERTIFICATE DB_URL DB_USERNAME DYNAMODB_CACHE_TABLE DYNAMODB_ENDPOINT FILESYSTEM_DISK LOG_CHANNEL LOG_DAILY_DAYS LOG_DEPRECATIONS_CHANNEL LOG_DEPRECATIONS_TRACE LOG_LEVEL LOG_PAPERTRAIL_HANDLER LOG_SLACK_EMOJI LOG_SLACK_USERNAME LOG_SLACK_WEBHOOK_URL LOG_STACK LOG_STDERR_FORMATTER LOG_SYSLOG_FACILITY MAIL_EHLO_DOMAIN MAIL_ENCRYPTION MAIL_FROM_ADDRESS MAIL_FROM_NAME MAIL_HOST MAIL_LOG_CHANNEL MAIL_MAILER MAIL_PASSWORD MAIL_PORT MAIL_SENDMAIL_PATH MAIL_URL MAIL_USERNAME MEMCACHED_HOST MEMCACHED_PERSISTENT_ID MEMCACHED_USERNAME MEMCACHED_PASSWORD MEMCACHED_PORT MYSQL_ATTR_SSL_CA PAPERTRAIL_PORT PAPERTRAIL_URL POSTMARK_MESSAGE_STREAM_ID POSTMARK_TOKEN QUEUE_CONNECTION QUEUE_FAILED_DRIVER REDIS_CACHE_CONNECTION REDIS_CACHE_DB REDIS_CACHE_LOCK_CONNECTION REDIS_CLIENT REDIS_CLUSTER REDIS_DB REDIS_HOST REDIS_PASSWORD REDIS_PORT REDIS_QUEUE REDIS_QUEUE_CONNECTION REDIS_QUEUE_RETRY_AFTER REDIS_URL REDIS_USERNAME SESSION_CONNECTION SESSION_DOMAIN SESSION_DRIVER SESSION_ENCRYPT SESSION_EXPIRE_ON_CLOSE SESSION_HTTP_ONLY SESSION_LIFETIME SESSION_PARTITIONED_COOKIE SESSION_PATH SESSION_SAME_SITE SESSION_SECURE_COOKIE SESSION_STORE SESSION_TABLE SLACK_BOT_USER_DEFAULT_CHANNEL SLACK_BOT_USER_OAUTH_TOKEN SQS_PREFIX SQS_QUEUE SQS_SUFFIX ``` ### 419 Page Expired error in Laravel Source: https://rocketeersapp.com/419-page-expired-laravel When working with Laravel you will encounter this error from time to time. Here's how you can fix this error. ## Why is the page expired? Laravel uses Cross-Site Request Forgery (CSRF) as a protection mechanism, that protects your app from external HTTP requests to your application. Requests from the outside cannot always be trusted, because they can try to mingle with the data and sessions of your users. CSRF works by generating a unique and randomly generated token that only your application knows and therefore it can detect if a request is allowed by verifying this token. The token expires automatically to make sure it cannot be retrieved and used again and again. ## When does this happen A page expired error can happen when you've forgotten to send the randomly generated CSRF token along with a "POST", "PUT", "PATCH", or "DELETE" request. This typically happens when making an AJAX request or when submitting a form. ## How to fix the error When submitting a form, always add a hidden input named `_token` with the value set to `csrf_token()`. More easily you can use the `@csrf` Blade directive which is a shortcut to output this hidden input. If you're performing an AJAX request, then it's because you've forgotten to add the `X-CSRF-TOKEN` header to the request. You can add this header automatically to every AJAX request when using the popular [Axios](https://axios-http.com) Javascript HTTP library: ```javascript window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; ``` Or when using jQuery: ```javascript $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); ``` If users hit the error after leaving a form open for a while, the session (and with it the token) has simply expired. Raise `SESSION_LIFETIME` in your [.env file](/environment-variables-laravel) and [clear the config cache](/clear-cache-laravel) afterwards. Another option - depending on your use case - is to [disable the verification of the CSRF token](/disable-csrf-in-laravel) for all or specific routes in your application. In case of stateless requests like API or webhooks this makes sense and is the use of API tokens or signed routes more suitable. For a deeper walkthrough of every cause and fix, including AJAX headers and expired sessions, see [CSRF token mismatch in Laravel](/csrf-token-mismatch-laravel). ### How to clear cache in Laravel Source: https://rocketeersapp.com/clear-cache-laravel Laravel uses caching for a lot of parts of the framework, here's how you can clear all of them. ## How to clear all caches at once To clear all caches at once, Laravel has a specific optimize command that can clear every major cache in the application. This includes the configured cache driver, the events, views, route, config and bootstrap files: ```bash php artisan optimize:clear ``` ## Application cache Laravel has versatile cache functionalities baked in the framework. It can handle multiple cache stores like file based, using a database (MySQL, PotgreSQL, SQLite, Redis) or specific software like memcached. Using the command line you can clear the default configured cache with: ```php php artisan cache:clear ``` But you can also define another store that's not the default, but is configurred and used within your app: ```php php artisan cache:clear file # other stores are: apc, array, database, file, memcached, redis, dynamodb, octane ``` Clearing the cache within application code is possible using: ```php use Illuminate\Support\Facades\Cache; Cache::flush(); # clear default cache Cache::store('memcached')->flush(); # clear cache for 'memcached' store ``` ## Framework cache Laravel has multiple ways to improve its performance by caching framework specific functionality. Here are all caches Laravel uses to bootstrap the framework as quickly as possible: ### Config cache Using `php artisan config:cache` during deployments, Laravel can optimize config files and make them as static as possble to quickly load the config on each request. This also means you cannot update config dynamically anymore using the helper `config([])`. You can clear the config cache using: ```php php artisan config:clear ``` ### Events cache Using `php artisan events:cache` when deploying, Laravel can collect all event files upfront, which makes loading of a lot of scatered events inside your application much faster. ```php php artisan events:clear ``` ### Routes cache To further optimize performance, you can use `php artisan routes:cache` to deploy your Laravel app with an optimized routes cache. This collects all registered routes and creates a static cache to match every route as fast as possible. Clearing the routes cache: ```php php artisan routes:clear ``` ### Views cache Laravel compiles the Blade syntax to PHP code when executing the views for rendering it in your application. This cache is stored in `storage/framework/views` and contains all Blade views compiled to PHP files. Clearing this cache can be done with: ```php php artisan views:clear ``` ## Use Laravel without cache ## Array or null cache driver To really have no cache at all, you can configure `array` or `null` as cache driver. So this means you have no store at all, only during the runtime of a request the cache will work. After each request the cache will be empty again. ```bash CACHE_DRIVER=null # or "array" ``` ### Complete list of Laravel events Source: https://rocketeersapp.com/laravel-events Laravel provides quite a lot of events that are fired by default, which makes it easy to hook into using listeners. Out of the box Laravel has a wide variety of events that are fired inside your application by default. These events can help you with hooking into functionality and listen for when things are happening. This makes Laravel easily extensible without much effort. ## How to list events in your Laravel app If you would like to quickly overview all Laravel events inside your own app, including listeners. Execute the following artisan command: ```bash php artisan event:list ``` ## Complete list of events in Laravel Here is an up-to-date overview per section of the latest Laravel version 10.x and official packages, with events per module. ### Laravel Auth events ```php Illuminate\Auth\Events\Attempting::class Illuminate\Auth\Events\Authenticated::class Illuminate\Auth\Events\CurrentDeviceLogout::class Illuminate\Auth\Events\Failed::class Illuminate\Auth\Events\Lockout::class Illuminate\Auth\Events\Login::class Illuminate\Auth\Events\Logout::class Illuminate\Auth\Events\OtherDeviceLogout::class Illuminate\Auth\Events\PasswordReset::class Illuminate\Auth\Events\Registered::class Illuminate\Auth\Events\Validated::class Illuminate\Auth\Events\Verified::class ``` ### Laravel Bus Events ```php Illuminate\Bus\Events\BatchDispatched::class ``` ### Laravel Cache events ```php Illuminate\Cache\Events\CacheEvent::class Illuminate\Cache\Events\CacheHit::class Illuminate\Cache\Events\CacheMissed::class Illuminate\Cache\Events\KeyForgotten::class Illuminate\Cache\Events\KeyWritten::class ``` ### Laravel Console events ```php Illuminate\Console\Events\ArtisanStarting::class Illuminate\Console\Events\CommandFinished::class Illuminate\Console\Events\CommandStarting::class Illuminate\Console\Events\ScheduledBackgroundTaskFinished::class Illuminate\Console\Events\ScheduledTaskFailed::class Illuminate\Console\Events\ScheduledTaskFinished::class Illuminate\Console\Events\ScheduledTaskSkipped::class Illuminate\Console\Events\ScheduledTaskStarting::class ``` ### Laravel Contracts events ```php Illuminate\Console\Contracts\ShouldDispatchAfterCommit::class Illuminate\Console\Contracts\ShouldHandleEventsAfterCommit::class ``` ### Laravel Database events ```php Illuminate\Database\Events\ConnectionEstablished::class Illuminate\Database\Events\ConnectionEvent::class Illuminate\Database\Events\DatabaseBusy::class Illuminate\Database\Events\DatabaseRefreshed::class Illuminate\Database\Events\MigrationEnded::class Illuminate\Database\Events\MigrationEvent::class Illuminate\Database\Events\MigrationStarted::class Illuminate\Database\Events\MigrationsEnded::class Illuminate\Database\Events\MigrationsEvent::class Illuminate\Database\Events\MigrationsStarted::class Illuminate\Database\Events\ModelPruningFinished::class Illuminate\Database\Events\ModelPruningStarting::class Illuminate\Database\Events\ModelsPruned::class Illuminate\Database\Events\NoPendingMigrations::class Illuminate\Database\Events\QueryExecuted::class Illuminate\Database\Events\SchemaDumped::class Illuminate\Database\Events\SchemaLoaded::class Illuminate\Database\Events\StatementPrepared::class Illuminate\Database\Events\TransactionBeginning::class Illuminate\Database\Events\TransactionCommitted::class Illuminate\Database\Events\TransactionCommitting::class Illuminate\Database\Events\TransactionRolledBack::class ``` ### Laravel Foundation events ```php Illuminate\Foundation\Events\LocaleUpdated::class Illuminate\Foundation\Events\MaintenanceModeDisabled::class Illuminate\Foundation\Events\MaintenanceModeEnabled::class Illuminate\Foundation\Events\PublishingStubs::class Illuminate\Foundation\Events\VendorTagPublished::class ``` ### Laravel HTTP client events ```php Illuminate\Http\Client\Events\ConnectionFailed::class Illuminate\Http\Client\Events\RequestSending::class Illuminate\Http\Client\Events\ResponseReceived::class ``` ### Laravel Log events ```php Illuminate\Log\Events\MessageLogged::class ``` ### Laravel Mail events ```php Illuminate\Mail\Events\MessageSending::class Illuminate\Mail\Events\MessageSent::class ``` ### Laravel Notifications events ```php Illuminate\Notifications\Events\BroadcastNotificationCreated::class Illuminate\Notifications\Events\NotificationFailed::class Illuminate\Notifications\Events\NotificationSending::class Illuminate\Notifications\Events\NotificationSent::class ``` ### Laravel Queue events ```php Illuminate\Queue\Events\JobExceptionOccurred::class Illuminate\Queue\Events\JobFailed::class Illuminate\Queue\Events\JobPopped::class Illuminate\Queue\Events\JobPopping::class Illuminate\Queue\Events\JobProcessed::class Illuminate\Queue\Events\JobProcessing::class Illuminate\Queue\Events\JobQueued::class Illuminate\Queue\Events\JobReleasedAfterException::class Illuminate\Queue\Events\JobRetryRequested::class Illuminate\Queue\Events\JobTimedOut::class Illuminate\Queue\Events\Looping::class Illuminate\Queue\Events\QueueBusy::class Illuminate\Queue\Events\WorkerStopping::class ``` ### Laravel Redis events ```php Illuminate\Redis\Events\CommandExecuted::class ``` ### Laravel Routing events ```php Illuminate\Routing\Events\PreparingResponse::class Illuminate\Routing\Events\ResponsePrepared::class Illuminate\Routing\Events\RouteMatched::class Illuminate\Routing\Events\Routing::class ``` ### Laravel Eloquent events Laravel Eloquent uses keys instead of FQDN. ```php eloquent.created eloquent.creating eloquent.deleted eloquent.deleting eloquent.forceDeleted eloquent.forceDeleting eloquent.restored eloquent.saved eloquent.saving eloquent.updated eloquent.updating ``` ## Official Laravel packages ### Laravel Folio events ```php Laravel\Folio\Events\ViewMatched::class ``` ### Laravel Fortify events ```php Laravel\Fortify\Events\PasswordUpdatedViaController::class Laravel\Fortify\Events\RecoveryCodeReplaced::class Laravel\Fortify\Events\RecoveryCodesGenerated::class Laravel\Fortify\Events\TwoFactorAuthenticationChallenged::class Laravel\Fortify\Events\TwoFactorAuthenticationConfirmed::class Laravel\Fortify\Events\TwoFactorAuthenticationDisabled::class Laravel\Fortify\Events\TwoFactorAuthenticationEnabled::class Laravel\Fortify\Events\TwoFactorAuthenticationEvent::class ``` ### Laravel Horizon events ```php Laravel\Horizon\Events\JobDeleted::class Laravel\Horizon\Events\JobFailed::class Laravel\Horizon\Events\JobPushed::class Laravel\Horizon\Events\JobReleased::class Laravel\Horizon\Events\JobReserved::class Laravel\Horizon\Events\JobsMigrated::class Laravel\Horizon\Events\LongWaitDetected::class Laravel\Horizon\Events\MasterSupervisorDeployed::class Laravel\Horizon\Events\MasterSupervisorLooped::class Laravel\Horizon\Events\MasterSupervisorOutOfMemory::class Laravel\Horizon\Events\MasterSupervisorReviving::class Laravel\Horizon\Events\RedisEvent::class Laravel\Horizon\Events\SupervisorLooped::class Laravel\Horizon\Events\SupervisorOutOfMemory::class Laravel\Horizon\Events\SupervisorProcessRestarting::class Laravel\Horizon\Events\UnableToLaunchProcess::class Laravel\Horizon\Events\WorkerProcessRestarting::class ``` ### Laravel Jetstream events ```php Laravel\Jetstream\Events\AddingTeam::class Laravel\Jetstream\Events\AddingTeamMember::class Laravel\Jetstream\Events\InvitingTeamMemberFlushed::class Laravel\Jetstream\Events\RemovingTeamMember::class Laravel\Jetstream\Events\TeamCreated::class Laravel\Jetstream\Events\TeamDeleted::class Laravel\Jetstream\Events\TeamEvent::class Laravel\Jetstream\Events\TeamMemberAdded::class Laravel\Jetstream\Events\TeamMemberRemoved::class Laravel\Jetstream\Events\TeamMemberUpdated::class Laravel\Jetstream\Events\TeamUpdated::class ``` ### Laravel Octane events ```php Laravel\Octane\Events\HasApplicationAndSandbox::class Laravel\Octane\Events\RequestHandled::class Laravel\Octane\Events\RequestReceived::class Laravel\Octane\Events\RequestTerminatedived::class Laravel\Octane\Events\TaskReceived::class Laravel\Octane\Events\TaskTerminated::class Laravel\Octane\Events\TickReceived::class Laravel\Octane\Events\TickTerminated::class Laravel\Octane\Events\WorkerErrorOccurred::class Laravel\Octane\Events\WorkerStarting::class Laravel\Octane\Events\WorkerStopping::class ``` ### Laravel Passport events ```php Laravel\Passport\Events\AccessTokenCreated::class Laravel\Passport\Events\RefreshTokenCreated::class ``` ### Laravel Pennant events ```php Laravel\Pulse\Events\AllFeaturesPurged::class Laravel\Pulse\Events\DynamicallyRegisteringFeatureClass::class Laravel\Pulse\Events\FeatureDeleted::class Laravel\Pulse\Events\FeatureResolved::class Laravel\Pulse\Events\FeatureRetrieved::class Laravel\Pulse\Events\FeatureUpdated::class Laravel\Pulse\Events\FeatureUpdatedForAllScopes::class Laravel\Pulse\Events\FeaturesPurged::class Laravel\Pulse\Events\UnexpectedNullScopeEncountered::class Laravel\Pulse\Events\UnknownFeatureResolved::class ``` ### Laravel Pulse events ```php Laravel\Pulse\Events\ExceptionReported::class Laravel\Pulse\Events\IsolatedBeat::class Laravel\Pulse\Events\SharedBeat::class ``` ### Laravel Sanctum events ```php Laravel\Sanctum\Events\TokenAuthenticated::class ``` ### Laravel Scout events ```php Laravel\Scout\Events\ModelsFlushed::class Laravel\Scout\Events\ModelsImported::class ``` To record what your listeners do, combine events with [Laravel logging](/laravel-logging). For caching expensive listener work, see the [Laravel cache](/laravel-cache). ### Laravel Valet Source: https://rocketeersapp.com/laravel-valet For Mac users Laravel Valet is an execellent tool to develop all your PHP projects locally. ## What is Laravel Valet? Laravel Valet is a tool that makes developing locally more enjoyable, because it makes your Mac instantly ready for development on your local computer. It's a minimalist tool, that uses Homebrew to install everything you need and get started quickly. Laravel Valet uses nginx, DnsMasq and PHP binaries to make it easy to develop using PHP and being able to switch to different PHP versions. ## Not only for Laravel apps Despite its name, Laravel Valet is not only for developing Laravel apps. Valet supports out of the box the following types of projects to run locally on your computer. - Laravel - Bedrock - CakePHP 3 - ConcreteCMS - Contao - Craft - Drupal - ExpressionEngine - Jigsaw - Joomla - Katana - Kirby - Magento - OctoberCMS - Sculpin - Slim - Statamic - Static HTML - Symfony - WordPress - Zend ## How to install Laravel Valet First ensure Homebrew is up to date with the latest software packages: ```bash brew update ``` Then, install PHP using: ```bash brew install php ``` Make sure that `~/.composer/vendor/bin` is in your system's PATH, so that after composer is installed, you can install Laravel Valet as a global package: ```bash composer global require laravel/valet ``` At last, install Valet using the `install` command: ```bash valet install ``` After that, you can use any \*.test domain to use locally on your projects. ## How does it work Laravel Valet uses a simple paradigm: every domain is a folder inside "parked" directory. So when you want to use the Mac folder `~/Sites` to clone all your projects on your computer in, you can `park` inside this directory to make all folders available as a `[folder].test` domain. ```bash cd ~/Sites valet park ``` ## Using different PHP versions Install different PHP versions you need for you local repositories, you can install additional PHP version using: ```bash brew install php@8.3 ``` Replace the version number with the version you want to install. After that you can tell Laravel Valet to use this PHP version for your project or as a new default PHP version for all sites. To use PHP 8.3 for all sites: ```bash valet use php@8.3 # use --force when this is not enough ``` Or only for a specific site: ```bash valet isolate php@8.3 # execute this inside the correct ~/Sites/[site] folder ``` You can also set a specific version for Laravel Valet to use, when using the `valet use` (without a PHP version) command. The PHP version can be determined from a `.valetrc` file in the root of the project: ```bash php=php@8.3 ``` After adding this file, you can execute `valet use` to always use the specified PHP version with Laravel Valet. On a production server you'd instead [install multiple PHP versions](/how-to-install-multiple-php-versions-on-same-server) and pick one per site. To confirm a project's framework release, [check your Laravel version](/check-laravel-version). ### Creating an encrypted cookie value in Laravel Source: https://rocketeersapp.com/creating-an-encrypted-cookie-value-in-laravel Laravel can encrypt cookies automatically using the `\App\Http\Middleware\EncryptCookies::class` middleware. It's so easy, you don't have to think about it when adding your cookies to the response. But when you want to hit your own application with a request that needs a certain cookie, you can't simply add the cookie to the HTTP request and you also cannot encrypt it using the `encrypt()` helper. This is because cookies need a different treatment when creating the exact encrypted cookie value. When digging in the source of Laravel we found the following code and we wrapped it up in a helper, because we have a specific use case for an application that needs to perform HTTP requests to itself, with some encrypted cookies. Here we go, this helper can be added to any file your keeping your little helpers in: ```php if (! function_exists('encrypted_cookie_value')) { function encrypted_cookie_value(string $name, string $value): string { $encrypter = app(Illuminate\Encryption\Encrypter::class); return $encrypter->encrypt( CookieValuePrefix::create($name, $encrypter->getKey()).$value, false ); } } ``` Keep the related secrets in your [environment variables](/environment-variables-laravel). If you'd rather switch cookies off entirely, see [disabling cookies in Laravel](/disable-cookies-in-laravel). ### Disable CSRF in Laravel Source: https://rocketeersapp.com/disable-csrf-in-laravel Sometimes you need to disable the CSRF token verification in Laravel. A common use case is when you want to receive POST webhooks. ## What is CSRF? Cross-Site Request Forgery (CSRF) is a protection mechanism. This security is added and enabled by default in Laravel. CSRF protects your app for requests from outside your application. It uses a random generated token that only your application knows and therefore it can detect if a request is allowed by verifying this token. ## Examples of not needing CSRF Sometimes you don't need the extra protection or want to do something that makes CSRF verification difficult. In case of receiving webhooks it is sometimes necessary and also when working with incoming API requests for routes that are not defined in the `routes/api.php` and therefore do not use the API middleware group. I'm not going to argue the best practices here, but I am going to show you how you can disable the CSRF token check in Laravel. If you landed here because of an error rather than webhooks, you probably want [CSRF token mismatch in Laravel](/csrf-token-mismatch-laravel) or the [419 Page Expired error](/419-page-expired-laravel) instead, disabling CSRF is rarely the right fix for those. ## Finding the CSRF middleware From Laravel v5.1 you can find the CSRF verification middleware inside `app/Http/Middleware/VerifyCsrfToken.php`. There you find a protected array variable `$except`. This array you can fill in different ways, here are some options: ## Disabling CSRF for every route You can disable CSRF completely for all routes in your application using the asterisk (\*) wildcard: ```php protected $except = [ '*', ]; ``` ## Disabling CSRF for path using wildcard Disabling a specific kind of path using a wildcard, is also possible: ```php protected $except = [ 'webhooks/*', ]; ``` ## Disabling CSRF for specific paths In this example only specific non-wildcard paths are defined and exempt from CSRF protection: ```php protected $except = [ 'webhooks/mailgun', 'webhooks/postmark', ]; ``` ### How to check which Laravel version of your app is using Source: https://rocketeersapp.com/check-laravel-version Check which Laravel version your app is running on. ## Command Line using Artisan If you want to know the exact version of your Laravel app, the easiest and fastest option is using the command line: ```bash php artisan --version # Example output: Laravel Framework 9.33.0 ``` ## Command Line using Composer It's also possible to determine the Laravel version using Composer, because Laravel is (or should be) installed using this package manager. Piping the JSON formatted output of Composer to `jq` makes the value directly accessible: ```bash composer show laravel/framework --format json | jq '.versions' # Example output: ["v9.33.0"] ``` ## Using the `app()` helper Within the application code, you can use the `app()` helper to discover the current Laravel version: ```php echo app()->version(); // Example output: 9.33.0 ``` Using the helper, you could show the Laravel version in a Blade template to expose it in a development environment or when a admin needs to have some debug info: ```php {{ app()->version() }} ``` Working locally on a Mac? [Laravel Valet](/laravel-valet) makes switching PHP and framework versions painless. Configuration per environment lives in your [environment variables](/environment-variables-laravel). ### Logging in Laravel Source: https://rocketeersapp.com/laravel-logging When you have an web app running, it is important to know what it does and how it's going. To get more insight into this, using the extensive logging features of Laravel is key. ## When you need logging When developing an application, you most likely want to understand what your code actually does. When your coding it is not always possible to understand what the outcome will be without testing it or logging the output to a certain place. By dispatching and processing jobs on a queue on the background, using scheduled commands (cronjobs) and other techniques the need for logging what the code does will be higher than working on a fairly simple app. ## How does logging work in Laravel Laravel uses the widely used Monolog package under the hood for all its logging capabilities. This logging solution in PHP is very extensive and flexible and Laravel has integrated it very well inside their framework. By using a comprehensive config for logging, you can combine multiple log streams in Laravel. This makes it easy to output to different locations and keeps the logging posibilities very flexible. Laravel makes this possible by using the terminology of channels. Channels are meant to provide multiple types of logging out of the box and using the `stack` driver you can configure multiple channels at the same time. So your application can log into multiple locations at once. ## Configuration The default configuration is a great example of how this works. Here are three different channels defined, a `syslog`, `slack` and `stack` driver. Where the last driver is a special one, because this can combine multiple channels (in this case `syslog` and `slack` together) at once: ```php 'channels' => [ 'stack' => [ 'driver' => 'stack', 'channels' => ['syslog', 'slack'], ], 'syslog' => [ 'driver' => 'syslog', 'level' => 'debug', ], 'slack' => [ 'driver' => 'slack', 'url' => env('LOG_SLACK_WEBHOOK_URL'), 'username' => 'Laravel Log', 'emoji' => ':boom:', 'level' => 'critical', ], ], ``` ## Extensible Anything is possible for logging in your Laravel application. You can [even write your own driver fairly easy](https://laravel.com/docs/9.x/logging#creating-custom-channels-via-factories) by writing a custom logger. ## Writing log statements When you want to write log statements to one or more channels, you can use the `Log` facade or the `logger()` helper inside your Laravel app. This makes it really easy to output any message you want to the configured log channels: ```php use Illuminate\Support\Facades\Log; Log::debug($message); // logger()->debug($message); ``` ## Using different log levels When logging messages, it is a best practice to define the correct level the message is intended for. For example, it could be handy to use `info` for only informational purposes. To follow along the execution of your code for example. But when you want to log critical errors to your log files, than it's best to also use the `critical()` method for this use case. Here are the methods and functions of every level that is available in Laravel: ```php use Illuminate\Support\Facades\Log; Log::emergency($message); // logger()->emergency($message); Log::alert($message); // logger()->alert($message); Log::critical($message); // logger()->critical($message); Log::error($message); // logger()->error($message); Log::warning($message); // logger()->warning($message); Log::notice($message); // logger()->notice($message); Log::info($message); // logger()->info($message); Log::debug($message); // logger()->debug($message); ``` Laravel also fires a wide range of [events](/laravel-events) you can hook into for the same visibility. When you need errors surfaced in real time, Rocketeers provides [site error reporting and management](/features/sites-error-reporting-and-management), with alerts to email, [Slack](/providers/slack), Discord, and Telegram. ### Disable cookies in Laravel Source: https://rocketeersapp.com/disable-cookies-in-laravel When you don't need cookies, it's a good practice to prevent your app from creating them in the first place. When you have a Laravel project that doesn't need cookies in any way, it's great to know how to completely prevent the usage of cookies. It makes your application cookieless and stateless. In Laravel they are send down the line by default and you may not need them or don't want them (in case of GDPR compliance for example). ## When Laravel needs cookies Laravel uses cookies by default to attach the session to the same visitor and to keep a CSRF-token for sending forms to your application. Because most applications will need to use cookies for authentication and/or cross site request forgery (CSRF) protection. > If you don't have authentication or forms on your website, than you don't need cookies and you can safely disable setting these cookies by Laravel. [→ How to disable CSRF in Laravel](/disable-csrf-in-laravel) ## How to prevent Laravel from using cookies To disable cookies the usage of cookies in Laravel, we need to make changes to the middleware stack that is setup by default. By default every visitor starts a new session and also has a unique CSRF token. To prevent this, we need to shift a few middlewares to a different middleware group. This is done in the code below, where we moved the middlewares to the new `cookies` group and removed them from `web`. ```php protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], 'api' => [ // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 'throttle:api', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], 'cookies' => [ \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, ], ]; ``` ## Only use cookies when you really need them When adding routes that do need authentication or have forms, you can now easily attach the `cookies` middleware group conditionally to these routes, to enable the necessary cookies: ```php Route::middleware('cookies')->group(function () { Route::get('login', [LoginController::class, 'form']); Route::post('login', [LoginController::class, 'login']); }); ``` In situations where you cannot use a route group because of packages or other restrictions, you can also enable them conditionally using the `boot()` method on the `AppServiceProvider` and push them to the default `web` middleware group based on a URL pattern: ```php public function boot() { // Only add cookies for requests within Laravel Nova if(request()->is('nova/*')) { $this->app['router']->pushMiddlewareToGroup('web', \Illuminate\Session\Middleware\StartSession::class); $this->app['router']->pushMiddlewareToGroup('web', \Illuminate\View\Middleware\ShareErrorsFromSession::class); $this->app['router']->pushMiddlewareToGroup('web', \App\Http\Middleware\VerifyCsrfToken::class); } } ``` If instead you need to *send* an encrypted cookie to your own application, see [creating an encrypted cookie value in Laravel](/creating-an-encrypted-cookie-value-in-laravel). ### How to use different PHP versions with Laravel Valet Source: https://rocketeersapp.com/different-php-versions-laravel-valet Laravel Valet is an awesome tool to quickly setup an development environment on your Mac. Here's how you can switch between different PHP versions while developing multiple projects. While Rocketeers app will allow you to use different PHP versions on your webservers, it previously was very difficult to use different PHP versions on your local machine using Laravel Valet. Since june 2022 Laravel Valet got a very nice upgrade in using different PHP versions and easily switch between them when working concurrently on different projects. ## Isolate a PHP version per project The first command that's new is `valet isolate` this command can be used to isolate a project using one specific PHP version. After that the nginx server that Valet installs on your Mac knows which PHP version to use for your project. For example the command for a project locked into using only PHP 8.1 is: ```bash valet isolate php@8.1 ``` ## Use the specific PHP version even in your command line Valet provides the ease of use to map local folders to hostnames in the browser and run them locally in the browser, using the correct PHP version. When using PHP from the command line, it is also possible to automatically let your Terminal know which PHP version to use when you run commands from the base path of your project. For this specific use case Valet offers an `valet php` command. This command aliases the PHP version within the folder of your project to the isolated PHP version. ## Use aliases for the ultimate DX To further optimize the DX you can make yourself even more comfortable. Include this alias in your `~/.zshrc` or `~/.bashrc` and you can keep using `php` to execute commands using the correct PHP version in your CLI: ```bash alias php="valet php" ``` In order to run also Composer on the same PHP version, set also an alias up with absolute path to Composer to keep using it like `composer require ...`: ```bash alias composer="php /usr/local/bin/composer" ``` See also [Laravel Valet](/laravel-valet) for the local development setup, and [installing multiple PHP versions](/how-to-install-multiple-php-versions-on-same-server) for production servers. ## Nginx ### Use nginx try_files to make your site static Source: https://rocketeersapp.com/nginx-try-files The `try_files` directive in nginx is incredibly powerful and useful when you want to make your dynamic website more performant. Learn how to leverage this to make your website fully static. ## How `try_files` works This directive makes use of a fallback system. The first (of possibly multiple) file paths that exists, will be used for the incoming HTTP request. So, for example you can set it to: ```bash try_files index1.html index2.php ``` If `index1.html` exists, it is served by nginx. If it does not exist, it will serve using the second file `index2.php`. ## How to use `try_files` for static or cached content As you already saw in the previous example, it is easy to put a static file in front of a dynamic file. But the only problem here is that in this case always `index.html` will be served. You can solve this by making the first entry dynamic using variables in nginx. Requests have multiple variables that are dynamic based on the specific request the web server is receiving. Some of these are: ```bash $request_method # (e.g. GET/HEAD/POST/PUT/DELETE) $scheme # http or https $host # domain $uri # path $query_string # query string (e.g. `?a=b`) ``` You can find all variables on the [official nginx website](https://nginx.org/en/docs/http/ngx_http_core_module.html). Using these variables, you can make a unique file location for each request. So this could be: ``` /cache/$host/$http_method/$uri?$query_string.html ``` This creates a path that points to the `cache` folder (relative to the document root) and inside this folder you have a custom path. ### $host In this example it begins with a `$host` folder, this is needed when you host multiple domains from the same virtual host. ### $request_method Then it creates a folder based on the $http_method (like GET or POST) and this is because of the simple reason we don't want to cache other requests than `GET`. So when creating the files, we only create a `/GET/` path inside this folder. ### $uri After that we have the `$uri` which can contain slashes and therefore creates folders, while the last segment is the file. So `https://rocketee.rs/category/nginx/try_files` would create the path `category/nginx` and the file `try_files`. #### $query_string Not necessary, but could be useful is adding the `$query_string` variable. This makes the request unique per different query string that is used. If the content on your webpages is not affected by query strings, you could remove it and have the same cache file respond to it. ## Configuring `try_files` Now putting this together, we got this configuration rule for `try_files`: ```bash try_files /cache/$host/$http_method/$uri?$query_string.html index.php ``` This checks first the cache path and if it does not exist it executes index.php. So when this happens, you can use the dynamic response of index.php to create a cached file. ## Creating the cache file using PHP In simple plain PHP this would look like this: ```php $response = '...'; // HTML response to cache $uri = $_SERVER['REQUEST_URI']; $path = parse_url($uri, PHP_URL_PATH); $query = parse_url($uri, PHP_URL_QUERY); file_put_contents( filename: "/cache/{$_SERVER['HTT_HOST']}/{$_SERVER['REQUEST_METHOD']}/{$path}?{$query}.html", data: $response, ); ``` This way you can have a completely dynamic website, that still leverages the fastest cache available (static files). If `try_files` sends requests round in circles, see [rewrite or internal redirection cycle](/nginx-rewrite-or-internal-redirection-cycle). For including content fragments at the server level, see [server side includes (SSI) in nginx](/server-side-includes-ssi-nginx). ### Log bot requests in nginx Source: https://rocketeersapp.com/log-bot-requests-nginx If you want to know when and how often bots visit your website, you can easily track this using the following configuration in nginx. ## Detect bots using nginx First we need to detect if the current visitor's user agent indicates it is a bot. We can do this with the `map` directive in nginx: ```bash map $http_user_agent $bot { default ""; "~*googlebot" "google"; "~*bingbot" "bing"; "~*slurp" "yahoo"; "~*duckduckbot" "duckduckgo"; "~*baiduspider" "baidu"; "~*yandexbot" "yandex"; "~*sogou" "sogou"; "~*exabot" "exabot"; "~*applebot" "apple"; "~*twitterbot" "twitter"; } ``` This checks the user agent string for matches for known bot names and then it maps it to a specific name that is set to variable `$bot`. ## Readble log format To make the entries readable, you can optionally choose to define a specific `log_format` for the bot requests: ```bash log_format bots "$time_local: $request_method $scheme://$host$request_uri [$status] $bytes_sent @ $request_time ($http_referer)"; ``` ## Log requests when it's a bot Now we can log these bot requests by creating a specific `bots.log` file using the `access_log` directive that logs requests only if `$bot` is filled and set the `log_format` to the newly created `bots` format. ```bash access_log /var/www/logs/bots.log bots if=$bot; ``` ## Log files per bot If you prefer separating the logs per bot, so you can more easily see how many times specifically the Googlebot has come by your website, you can define a variable per bot: ```bash if ($bot = "google") { set $google "1"; } if ($bot = "bing") { set $bing "1"; } if ($bot = "yahoo") { set $yahoo "1"; } if ($bot = "duckduckgo") { set $duckduckgo "1"; } if ($bot = "baidu") { set $baidu "1"; } if ($bot = "yandex") { set $yandex "1"; } if ($bot = "sogou") { set $sogou "1"; } if ($bot = "exabot") { set $exabot "1"; } if ($bot = "apple") { set $apple "1"; } if ($bot = "twitter") { set $twitter "1"; } ``` And therefore setup log files per bot: ```bash access_log /var/www/logs/bots/google.log bots if=$google; access_log /var/www/logs/bots/bing.log bots if=$bing; access_log /var/www/logs/bots/yahoo.log bots if=$yahoo; access_log /var/www/logs/bots/duckduckgo.log bots if=$duckduckgo; access_log /var/www/logs/bots/baidu.log bots if=$baidu; access_log /var/www/logs/bots/yandex.log bots if=$yandex; access_log /var/www/logs/bots/sogou.log bots if=$sogou; access_log /var/www/logs/bots/exabot.log bots if=$exabot; access_log /var/www/logs/bots/apple.log bots if=$apple; access_log /var/www/logs/bots/twitter.log bots if=$twitter; ``` To match a single well-behaved crawler instead, see [detecting Googlebot in nginx](/detect-googlebot-nginx). ### Detect Googlebot visits using nginx Source: https://rocketeersapp.com/detect-googlebot-nginx How to detect when and how often Googlebot visits your website using a few configuration lines in nginx. ## Detecting Googlebot It's very easy to detect the Googlebut using the user agent in nginx, here we use the `map` directive to set the variable `$googlebot` to `yes` or keep it empty depending on the given user agent: ```bash map $http_user_agent $googlebot { default ""; "~*googlebot" "yes"; } ``` ## Logging the requests When `$googlebot` is filled, we want to log the request in a log file. This can be done using the `access_log` directive: ```bash access_log /var/www/logs/googlebot.log bots if=$googlebot; ``` That's it! You can read here [how you can log multiple bots](/log-bot-requests-nginx) how to log multiple bots for your virtual host in nginx. ### How to install Nginx on Ubuntu Source: https://rocketeersapp.com/how-to-install-nginx Nginx is the web server that sits in front of your application and answers every request. Here is how to install it on Ubuntu, serve your first site, and set it up the way a production server should be. [Nginx](https://nginx.org) is the piece that listens on ports 80 and 443, terminates TLS, serves your static files, and passes everything else to [PHP-FPM](/how-to-install-php) or your application. It's fast, lightweight, and runs the majority of the busy sites on the web. Here's how to get it running on a fresh Ubuntu server. ## Install Nginx The quickest route is the package in Ubuntu's own repository: ```bash sudo apt-get update DEBIAN_FRONTEND=noninteractive sudo apt-get install -y nginx ``` If you want the latest stable release rather than whatever Ubuntu shipped, add the official Nginx repository first: ```bash echo "deb http://nginx.org/packages/ubuntu/ $(lsb_release -sc) nginx" \ | sudo tee /etc/apt/sources.list.d/nginx.list ``` Either way, start it and have it come back automatically after a reboot: ```bash sudo systemctl enable --now nginx ``` Visit your server's IP address in a browser and you should see the default Nginx welcome page. ## Open the firewall If you're running `ufw`, Nginx ships profiles that open the right ports. Allow HTTP and HTTPS: ```bash sudo ufw allow 'Nginx Full' ``` Without this, your site is reachable from the server itself but nothing else — a common reason a freshly installed site "doesn't load." ## Understand the directory layout A production Nginx setup keeps one config file per site and switches them on by symlink: ```bash sudo mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled ``` - `sites-available/` holds a `.conf` file for every site, whether it's live or not. - `sites-enabled/` holds symlinks to the ones that are actually active. Make sure the main `nginx.conf` includes the enabled sites (most distributions already do): ```nginx include /etc/nginx/sites-enabled/*; ``` ## Serve your first site Create a server block for your domain in `/etc/nginx/sites-available/example.com.conf`: ```nginx server { listen 80; server_name example.com www.example.com; root /var/www/example.com/public; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass unix:/var/run/php/php8.4-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } } ``` The `try_files` directive is the heart of most routing problems — if you hit a wall, read [understanding the Nginx try_files directive](/nginx-try-files). Enable the site by symlinking it, test the config, and reload: ```bash sudo ln -s /etc/nginx/sites-available/example.com.conf /etc/nginx/sites-enabled/ sudo nginx -t sudo service nginx reload ``` Always run `sudo nginx -t` before reloading. A single typo can take every site on the server down with a [502 Bad Gateway](/502-bad-gateway-nginx) or [403 Forbidden](/403-forbidden-nginx). ## Harden TLS with a strong DH group If you'll terminate HTTPS on this server (see [how to install Certbot](/how-to-install-certbot)), generate a strong Diffie-Hellman parameter file once, up front — it takes a while on 4096 bits but only has to happen one time: ```bash sudo openssl dhparam -dsaparam -out /etc/nginx/dhparam.pem 4096 ``` Reference it in your TLS config to push toward an [A+ SSL grade](/a-plus-grade-ssl-using-cloudflare). While you're tuning, it's worth [enabling gzip compression](/enable-gzip-compression-nginx) too. ## Let Rocketeers handle it Installing Nginx takes a few minutes. Running it well is the ongoing job: per-site server blocks, FastCGI tuning, the right PHP socket per site, compression, security headers, a strong DH group, and syncing real visitor IPs when you sit behind Cloudflare. Rocketeers provisions [Nginx the production way](/features/nginx-configured-for-optimal-performance) and generates a correct, tested vhost for every site you deploy — so you never hand-edit a config file or reload a broken one. ### nginx rewrite or internal redirection cycle Source: https://rocketeersapp.com/nginx-rewrite-or-internal-redirection-cycle This error means nginx kept redirecting a request back to itself until it gave up. Almost always a try_files directive that loops, producing a 500 error. ## About the error The page returns a 500 and the nginx error log shows: ```bash rewrite or internal redirection cycle while internally redirecting to "/index.php" ``` nginx tried to resolve a request, that resolution pointed at another location, which pointed back, and so on. After 10 internal redirects nginx assumes a loop and aborts with a 500. ## Why do I see this error The classic cause is a `try_files` that, when nothing matches, falls back to a target that itself triggers the same `try_files` again. For a PHP app it usually means: - The fallback file (`index.php`) doesn't exist at the expected path, so the fallback re-enters the same block. - The `root` is wrong, so nginx never finds the real file and keeps redirecting. - A `try_files` pointing at a named location or URI that loops back. ## Solution ### Use the standard Laravel/PHP try_files A correct front-controller setup looks like this. Note the `$uri` and `$uri/` are tried first, and the final fallback passes the path as a query string rather than re-requesting a file: ```nginx server { root /var/www/html/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass unix:/run/php/php8.3-fpm.sock; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; } } ``` ### Check the root actually contains index.php The most common real cause is a wrong `root`. If `/var/www/html/public/index.php` doesn't exist, the fallback can never resolve and loops: ```bash ls -l /var/www/html/public/index.php ``` Point `root` at the directory that genuinely contains the front controller (for Laravel that's `public`, not the project root). Validate and reload after fixing: ```bash nginx -t && systemctl reload nginx ``` For a deeper look at how `try_files` resolves requests, see [the nginx try_files article](/nginx-try-files). A redirect loop that happens in the browser rather than inside nginx shows up as [ERR_TOO_MANY_REDIRECTS](/err-too-many-redirects) instead. ### nginx: upstream sent too big header Source: https://rocketeersapp.com/nginx-upstream-sent-too-big-header This 502 variant happens when your application sends response headers larger than the buffers nginx reserves for them. Common with big cookies, large session data, or many headers. ## About the error The visitor gets a [502 Bad Gateway](/502-bad-gateway-nginx) and the nginx error log explains why: ```bash upstream sent too big header while reading response header from upstream ``` nginx reserves a fixed-size buffer for the response headers it reads back from your application (PHP-FPM or a proxied service). If the headers don't fit, nginx can't process the response and returns a 502. ## Why do I see this error - **Large cookies**, especially a big session cookie or many cookies set at once. - A long `Set-Cookie` from a fat session payload. - Many or unusually large custom headers. - A redirect with a very long `Location` URL. It frequently appears right after login, when the session and its cookie grow. ## Solution Increase the FastCGI buffer sizes so the headers fit. For a PHP-FPM app: ```nginx fastcgi_buffer_size 32k; fastcgi_buffers 8 16k; fastcgi_busy_buffers_size 64k; ``` For a proxied application (`proxy_pass`), the equivalents are: ```nginx proxy_buffer_size 32k; proxy_buffers 8 16k; proxy_busy_buffers_size 64k; ``` Put these in the relevant `location` or `server` block, then validate and reload: ```bash nginx -t && systemctl reload nginx ``` ### Fix the root cause too Bigger buffers treat the symptom. If the headers are huge because you're storing a lot in the session (and therefore the cookie), trim it. In Laravel, keep large data server-side by using a non-cookie session driver such as `redis` or `database` rather than the `cookie` driver, so only a small session id travels in the header: ```ini SESSION_DRIVER=redis ``` See [Redis connection refused in Laravel](/redis-connection-refused-laravel) if you switch to the Redis driver. ### Configure Content Security Policy with nonce using nginx Source: https://rocketeersapp.com/content-security-policy How to configure Content Security Policy (CSP) with secure and easy to configure nonces in nginx. ## What is a Content Security Policy (CSP) A Content Security Policy (CSP) is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross Site Scripting (XSS) and data injection attacks. These attacks are used for everything from data theft to site defacement or distribution of malware. In simple terms, CSP is a list of approved sources for the types of resources that can be loaded on a page. For example, you can specify that images may only come from a certain domain, or that scripts may only come from another domain. When a browser requests a page, it will only load or run scripts/resources from the approved domains. ## What is a nonce A nonce is a random string that is generated for each request. It is used to uniquely identify a request and is often used to prevent CSRF attacks. The nonce can be attached to the tags of scripts and stylesheets, to make sure only these tags are allowed to be parsed by the browser. ## Where to define CSP on On multiple levels you can define your CSP configuration, you can define it on the server, on the application level or heck, even on the client side in your HTML. But the problem with defining CSP on client side, is that full static caching is not possible anymore. The choice where to put your CSP configuration is based mostly on the situation you need it for. When you want every request secured the same way and have a lot of static content you need to cache, the server level is the place to define CSP. Because when requests aren't always touching the application layer, it is really the only place you can configure CSP. But when you have a lot of dynamics and need to define CSP flexibly, the application level is more convienent. When it's about performance, the server level is unbeaten. Especially someone like me who is a big fan of static caching, the server level is the way to go. ## Requirements We need a server with nginx installed and the sub_filter module enabled. The sub_filter module is used to replace the nonce placeholder with the generated nonce. How to check if the sub_filter module is enabled: ```bash nginx -V 2>&1 | tr ' ' '\n' | grep -qi 'http_sub_module' && echo "installed" || echo "not installed" ``` If the module is present, the ouput of the command above will be `installed`. ## Configuring nginx ### We need a nonce A nonce should be a lenghty random string and therefore unique for each request. In nginx we could create a variable containing a unique random string, but this ends up installing extensions to be able to do so. So instead we use the SSL session ID as a nonce. This is a unique string for each request and is already available in nginx. Fortunately the session ID contains only characters that are allowed in a nonce. ```bash set $cspNonce $ssl_session_id; ``` ### Injecting the nonce into the response Now we have a nonce, we need to inject it into the responses that nginx serves: ```bash sub_filter_once off; sub_filter_types *; sub_filter NGINX_CSP_NONCE $cspNonce; ``` Here we use `NGINX_CSP_NONCE` as the placeholder for the nonce. This placeholder can be used in the output of your application, and as long it goes through nginx, it will be replaced with a fresh and unique nonce: ```html ``` ### Sending the nonce with the CSP headers Now to apply the nonce to the CSP headers sent by nginx to the client, we can use the `add_header` directive: ```bash add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$cspNonce'; style-src 'self' 'nonce-$cspNonce' always"; ``` This CSP policy indicates that every source should come from the same domain as the page itself. The scripts and stylesheets should also contain the nonce that was generated for this request, to verify that only scripts and styles are loaded that are meant to load. That's it, now you have a CSP policy that is unique for each request and is compatible with full static caching. There are a lot more options to configure CSP, but this is the basic setup. More rules can be found at [Content Security Policy (CSP) Quick Reference Guide](https://content-security-policy.com/). ### Server Side Includes (SSI) in nginx Source: https://rocketeersapp.com/server-side-includes-ssi-nginx Server Side Includes can be a very handy feature when dealing with caching or including (dynamic) files into static files. Here's how to use it and configure nginx to enable the power of SSI. ## What are Server Side Includes (SSI) Server Side Includes (SSI) is a simple scripting language used on web servers to include content dynamically in web pages. SSI directives are embedded within HTML pages and are processed by the web server before the page is sent to the client's browser. The server executes the directives and includes the specified content in the final HTML document that is delivered to the user. SSI is typically used for tasks such as: 1. Including Content: You can include the content of one file into another. This is useful for creating reusable components or headers and footers that appear on multiple pages. ```html ``` 2. Date and Time Stamps: You can insert the current date and time into your web pages. ```html ``` 3. Conditional Statements: SSI supports simple conditional statements, allowing you to include or exclude content based on certain conditions. ```html Content for query string 1. Content for other cases. ``` 4. Variable Setting and Displaying: You can set variables and display their values. ```html <!--#echo var="pageTitle" --> ``` To use SSI, your web server needs to be configured to recognize and process SSI directives. The file extension ".shtml" is often associated with SSI-enabled files, but the configuration can vary depending on the server software being used (e.g., Apache, nginx). Make sure that the server administrator has enabled SSI processing for the desired file extensions. ## When can SSI be useful? While including files into another file is a typical task for dynamic scripting languages, there are some situations that SSI is comes in handy. The following situations suit SSI very well: 1. Hosting environment where scripting languages poses a security problem 2. The hosting can't be configured for executing server side scripting 3. When the file that needs to have includes is a static (HTML) file 4. The server resources are limited; SSI is very performant at high traffic 5. It is straightforward, lightweight and makes HTML a little bit dynamic 6. You have no excuses anymore for an outdated copyright year number in the footer ## How to enable SSI in nginx ### Add nginx apt repository ```bash echo "deb http://nginx.org/packages/ubuntu/ $(lsb_release -sc) nginx deb-src http://nginx.org/packages/ubuntu/ $(lsb_release -sc) nginx" > /etc/apt/sources.list.d/nginx.list sudo curl -L https://nginx.org/keys/nginx_signing.key | sudo apt-key add - sudo apt-get update ``` ### Install nginx using nginx-full ```bash sudo apt install -y nginx-full ``` Setting up a fresh server? The full setup is covered in [how to install nginx](/how-to-install-nginx). ### Configure SSI Add in the location block that needs to use SSI, the following rule: ```bash location / { ... ssi on; ... } ``` ### Reload nginx service Reload the nginx service to apply the configuration changes without downtime: ```bash sudo service nginx reload ``` Since SSI assembles plain HTML pages, it pairs well with [gzip compression in nginx](/enable-gzip-compression-nginx) to keep those pages small on the wire. ## PHP ### Enable JIT in PHP 8.x OPcache Source: https://rocketeersapp.com/php-8-opcache-jit Using OPcache can greatly improve [PHP performance](/php-performance). By enabling Just In Time (JIT) compiling in OPcache you can improve it even more. ## What is JIT (Just In Time) compiling? JIT is a compiler optimization feature introduced in PHP 8 and can enable faster execution times for CPU intensive tasks. It enhances performance by translating frequently executed PHP code into machine code at runtime, allowing it to be executed directly by the CPU instead of being interpreted line-by-line. This results in significant speed improvements! This is one of those extensions that's a bit confusing to setup, but it's not very dificult if you know how to. ## First: OPcache should be installed To begin with, we expect to have OPcache installed on your server. This is a PHP extension and on a Ubuntu server you can install it using (change to your current PHP version accordingly): ```bash sudo apt install php8.4-opcache ``` ## How to enable JIT compiling By default JIT is disabled in OPcache, so you need to enable it manually. Also note that the enabling of OPcache is separate for the PHP (FPM) process and using PHP on the CLI. JIT can be enabled by setting `opcache.jit_buffer_size` to a value and in general `128M` is a pretty decent value that's enough for most PHP applications. Add this line: ```ini opcache.jit_buffer_size=128M ``` To your `php.ini` config file or in `/etc/php/8.4/mods-available/opcache.ini`: ```bash opcache.enabled=1 opcache.jit_buffer_size=128M ``` Now JIT is enabled! ## Optimization level Next you need to decide a proper `opcache.jit` value that determines what optimizations the JIT compiler will perform. This can be precisely set as a bitmasker using a 4-digit integer "CRTO". More on this in the [PHP docs](https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.jit). But for typical usage, it's easier to use the following presets that cover most use cases. The following options: ```php # Completely disabled, cannot be enabled at runtime. opcache.jit=disable # Disabled, but can be enabled at runtime. opcache.jit=off # Use tracing JIT. Enabled by default and recommended for most users. opcache.jit=on # or opcache.jit=tracing # Use function JIT. opcache.jit=function ``` For general usage you can choose `on` which is almost the highest setting (1254) to enable almost all optimization levels. This makes the config now: ```bash opcache.enable=1 opcache.jit=on opcache.jit_buffer_size=128M ``` ## Enable in the CLI If you also want to optimize CLI usage, you can enable OPcache (and JIT) by adding: ```bash opcache.enable_cli=1 ``` ### How to install multiple PHP versions on the same server Source: https://rocketeersapp.com/how-to-install-multiple-php-versions-on-same-server One server, several sites, different PHP versions. Here is how to install PHP 8.2, 8.3, and 8.4 side by side and point each site at the version it needs. Not every site upgrades on the same schedule. One app needs PHP 8.4, an older one is stuck on 8.2, and they both live on the same server. The good news: PHP-FPM runs a separate service and socket per version, so you can install as many as you like and route each site to the right one. ## Install the versions you need With the [ondrej/php PPA](/how-to-install-php) added, install each version's FPM and CLI packages. Just repeat the install for every version: ```bash sudo add-apt-repository ppa:ondrej/php -y sudo apt-get update DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \ php8.2-fpm php8.2-cli php8.2-mysql php8.2-mbstring php8.2-xml php8.2-curl \ php8.3-fpm php8.3-cli php8.3-mysql php8.3-mbstring php8.3-xml php8.3-curl \ php8.4-fpm php8.4-cli php8.4-mysql php8.4-mbstring php8.4-xml php8.4-curl ``` Each version installs its own FPM service and its own socket: ```bash sudo systemctl status php8.2-fpm php8.3-fpm php8.4-fpm ``` ```bash /var/run/php/php8.2-fpm.sock /var/run/php/php8.3-fpm.sock /var/run/php/php8.4-fpm.sock ``` ## Point each site at a version This is the key step: in each site's [Nginx server block](/how-to-install-nginx), set `fastcgi_pass` to the socket of the version that site should use. A site on PHP 8.4: ```nginx location ~ \.php$ { include fastcgi_params; fastcgi_pass unix:/var/run/php/php8.4-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } ``` A different site on PHP 8.2 — same block, different socket: ```nginx fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; ``` Reload Nginx after editing, and each site runs on its own version simultaneously: ```bash sudo nginx -t && sudo service nginx reload ``` ## Set the CLI default The command line `php` is separate from what your sites use over FPM. Pick which version `php` resolves to on the command line with `update-alternatives`: ```bash sudo update-alternatives --set php /usr/bin/php8.4 ``` Check it: ```bash php -v ``` You can still call any version explicitly — `php8.2 artisan migrate`, `php8.3 -v` — regardless of the default. ## Turn off the ones you don't use Every running FPM pool holds memory. If a version is installed but no site uses it, stop and disable its service so it isn't sitting idle — see [disabling unused PHP-FPM pools](/disable-unused-php-fpm-pools). Developing locally on a Mac? [Laravel Valet can switch PHP versions](/different-php-versions-laravel-valet) the same way for your local sites. ## Let Rocketeers handle it Installing versions, remembering which socket maps to which site, keeping the CLI default straight, and shutting down idle pools is exactly the kind of bookkeeping that drifts out of sync over time. Rocketeers installs every PHP version you need on a server and lets each site [choose its version with a click](/features/multiple-php-versions-per-server) — the FPM socket wiring and the cleanup happen for you. ### How to install PHP on Ubuntu Source: https://rocketeersapp.com/how-to-install-php A modern PHP install means more than one apt package — you need PHP-FPM and the right set of extensions for your application. Here is how to install PHP on Ubuntu and wire it into Nginx. Ubuntu's default repositories are usually a PHP version or two behind, and they don't always carry every extension you'll need. The standard fix is Ondřej Surý's PPA, which packages every current PHP release for Ubuntu and keeps them up to date. Here's how to install PHP properly for a web server. ## Add the PHP repository Add the PPA that almost every production PHP server uses: ```bash sudo add-apt-repository ppa:ondrej/php -y sudo apt-get update ``` ## Install PHP-FPM and the extensions you need For a web server you want PHP-FPM (the FastCGI process manager [Nginx](/how-to-install-nginx) talks to), the CLI, and the common extensions. This installs PHP 8.4 with the set a typical Laravel or modern PHP app expects: ```bash DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \ php8.4-fpm php8.4-cli php8.4-curl php8.4-mbstring php8.4-xml php8.4-zip \ php8.4-mysql php8.4-pgsql php8.4-sqlite3 php8.4-gd php8.4-intl php8.4-bcmath \ php8.4-redis php8.4-opcache php8.4-readline ``` Swap `8.4` for whichever version you need — `8.3`, `8.2`, and so on are all available from the same PPA. ## Verify the install Check the CLI version and that the FastCGI service is running: ```bash php -v sudo systemctl status php8.4-fpm ``` PHP-FPM listens on a Unix socket at `/var/run/php/php8.4-fpm.sock`. That's the path your Nginx server block passes requests to with `fastcgi_pass`. ## Connect PHP to Nginx In your site's server block, hand `.php` files to the FPM socket: ```nginx location ~ \.php$ { include fastcgi_params; fastcgi_pass unix:/var/run/php/php8.4-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } ``` Test and reload Nginx, then drop a `phpinfo()` file in your web root to confirm PHP is executing: ```bash sudo nginx -t && sudo service nginx reload ``` If you see the raw PHP source instead of a rendered page, the `location ~ \.php$` block isn't matching — that's the usual cause of "PHP code showing in the browser." ## Tune it for production The default `php.ini` is conservative. Most sites need a higher [memory limit](/increase-php-memory-limit), realistic [upload limits](/php-file-upload-exceeds-upload-max-filesize), and [OPcache enabled](/enable-opcache-php) for speed. We walk through the settings that matter in [important PHP config options](/important-php-config-options). ## Install Composer Most PHP applications need [Composer](https://getcomposer.org) for dependencies: ```bash curl -sS https://getcomposer.org/installer | php sudo mv composer.phar /usr/local/bin/composer ``` ## Run more than one version Different sites often need different PHP versions on the same box. That's entirely possible — see [how to install multiple PHP versions on the same server](/how-to-install-multiple-php-versions-on-same-server). And once it's installed, here's [how to run PHP files](/run-php-files). ## Let Rocketeers handle it Picking the right extension set, keeping FPM tuned, enabling OPcache, installing Composer, and doing it again for every PHP version your sites need is fiddly, repetitive work. Rocketeers provisions PHP with the full production extension set, tunes `php.ini` and FPM sensibly, and lets each site pick its own [PHP version](/features/multiple-php-versions-per-server) — no PPAs or socket paths to remember. ### Important PHP config options Source: https://rocketeersapp.com/important-php-config-options The default php.ini is built to be safe, not to run a real website. These are the settings that actually matter in production and what to set them to. After you [install PHP](/how-to-install-php), it works — but the defaults in `php.ini` are conservative placeholders, not production values. A handful of settings make the difference between a site that runs smoothly and one that throws cryptic errors under load. Here are the ones worth knowing. ## Find your php.ini There's a separate `php.ini` per version and per SAPI (CLI vs FPM). Ask PHP where each one lives: ```bash php --ini ``` For a web server, the file you care about is the FPM one, usually `/etc/php/8.4/fpm/php.ini`. Restart FPM after any change: ```bash sudo service php8.4-fpm restart ``` ## memory_limit How much memory a single script may use. The default of `128M` is tight for modern frameworks and image processing. `256M` or `512M` is a common production value: ```ini memory_limit = 512M ``` Set it too low and you get [allowed memory size exhausted](/php-allowed-memory-size-exhausted). More detail in [increase PHP memory limit](/increase-php-memory-limit). ## max_execution_time The longest a script may run before PHP kills it. The default `30` seconds is fine for web requests — keeping it low protects you from runaway scripts. Raise it only for specific long jobs, and prefer queues over long requests: ```ini max_execution_time = 30 ``` Hitting the limit produces [maximum execution time exceeded](/php-maximum-execution-time-exceeded). ## upload_max_filesize and post_max_size These cap file uploads, and they work together — `post_max_size` must be at least as large as `upload_max_filesize`, because the upload travels inside the POST body: ```ini upload_max_filesize = 64M post_max_size = 64M ``` If uploads fail silently, this pair is almost always why — see [POST file exceeds upload_max_filesize](/php-file-upload-exceeds-upload-max-filesize). ## error_reporting and display_errors This is the setting people get backwards most often. On a **production** site, never show errors to visitors — log them instead: ```ini display_errors = Off log_errors = On error_reporting = E_ALL ``` On a **development** machine you want the opposite, so you actually see what broke: ```ini display_errors = On ``` Leaking a stack trace to the public is both ugly and a security risk. Keep it off in production. ## date.timezone An unset timezone makes PHP guess and emit warnings. Set it explicitly — UTC is the safe default for servers: ```ini date.timezone = UTC ``` ## opcache OPcache compiles your PHP to bytecode and keeps it in memory, which is one of the biggest free performance wins available. Make sure it's on: ```ini opcache.enable = 1 opcache.memory_consumption = 256 opcache.max_accelerated_files = 20000 ``` Full walkthrough in [enabling OPcache](/enable-opcache-php), and on PHP 8 you can go further with [the OPcache JIT](/php-8-opcache-jit). ## FPM self-healing In `php-fpm.conf`, these let the FPM master restart workers automatically if they start crashing, instead of taking the pool down with them: ```ini emergency_restart_threshold = 10 emergency_restart_interval = 1m process_control_timeout = 30s ``` ## Keep secrets out of php.ini Configuration that changes between environments — database credentials, API keys — belongs in environment variables, not `php.ini` or your code. See [environment variables in Laravel](/environment-variables-laravel) for the pattern. ## Let Rocketeers handle it Every one of these has a sensible production value, a separate file per PHP version, and an FPM restart to make it stick — and [getting `display_errors` wrong on production](/features/turn-off-debug-mode-automatically-on-production) is a real security hole. Rocketeers tunes `php.ini` and FPM to production-ready values for every PHP version it installs, so the defaults you start with are already the right ones. ### PHP Warning: Undefined array key Source: https://rocketeersapp.com/undefined-array-key-php PHP 8 turned accessing a missing array key into a warning instead of a silent notice. The fix is to check the key exists or provide a default before reading it. ## About the error ```bash PHP Warning: Undefined array key "email" in /var/www/app.php on line 12 ``` You read `$array['email']` but that key doesn't exist. In PHP 7 this was a quiet `E_NOTICE` many people never saw; **PHP 8 promoted it to an `E_WARNING`**, so upgrading a codebase surfaces a flood of these. A closely related message is `Trying to access array offset on value of type null`, which means the thing you indexed wasn't an array at all. ## Why do I see this error - The key really isn't there (an optional form field, a missing query parameter). - A typo in the key name. - The variable is `null` rather than an array, so there's no key to read. - Code that "worked" on PHP 7 because the notice was hidden. ## Solution ### Provide a default with the null coalescing operator The cleanest fix is `??`, which returns the right-hand side when the key is missing or null: ```php $email = $_POST['email'] ?? null; $page = $_GET['page'] ?? 1; ``` ### Check existence explicitly When you need to branch on whether the key is present: ```php if (array_key_exists('email', $data)) { // present, even if its value is null } if (isset($data['email'])) { // present and not null } ``` Note the difference: `isset()` treats a key with a `null` value as "not set", while `array_key_exists()` only checks the key is there. ### In Laravel Use `data_get()` or request helpers, which return `null` (or a default) instead of warning: ```php $value = data_get($array, 'user.email', 'unknown'); $page = $request->input('page', 1); ``` If the underlying value is an object rather than an array and you're calling a method on it, see [Call to a member function on null](/call-to-a-member-function-on-null). And if PHP can't find a class at all, see [Class not found](/php-class-not-found). ### PHP file upload exceeds upload_max_filesize Source: https://rocketeersapp.com/php-file-upload-exceeds-upload-max-filesize When a file upload silently fails or is empty, PHP itself is usually rejecting it because it is larger than upload_max_filesize or post_max_size. This is the PHP-side companion to the nginx 413 error. ## About the error Unlike most errors, this one is quiet. The upload just doesn't arrive, `$_FILES` is empty, or you see `UPLOAD_ERR_INI_SIZE` in the file's `error` key. PHP discarded the file before your code ever ran. This is distinct from, but often confused with, the [413 Request Entity Too Large](/413-request-entity-too-large) error. That one is nginx rejecting the request before it reaches PHP. If you've already raised nginx's `client_max_body_size` and uploads still fail, PHP's own limits are the next place to look. ## Why do I see this error Two PHP settings cap upload size, and the *lower* of the two wins: - `upload_max_filesize`, the maximum size of a single uploaded file. - `post_max_size`, the maximum size of the entire POST body (which must be larger than `upload_max_filesize`, with room for other fields). If either is smaller than the file, PHP drops it. ## Solution Raise both values in your `php.ini`. `post_max_size` should be a bit larger than `upload_max_filesize`: ```ini upload_max_filesize = 100M post_max_size = 110M ``` While you're there, check these related limits don't cut a large upload short: ```ini max_file_uploads = 20 max_execution_time = 120 memory_limit = 256M ``` Reload PHP-FPM so the changes take effect: ```bash systemctl reload php8.3-fpm ``` ### Don't forget the layers in front of PHP A large upload has to pass through every layer to succeed. All of these must be big enough: - **nginx**, via `client_max_body_size`, see [413 Request Entity Too Large](/413-request-entity-too-large). - **PHP**, via `upload_max_filesize` and `post_max_size` above. - **Your application's own validation**, for example a Laravel `max:` rule on the file. Verify the active PHP values with: ```bash php -i | grep -E 'upload_max_filesize|post_max_size' ``` Note that the CLI and the FPM SAPI can use different `php.ini` files, so check the FPM one (`/etc/php/8.3/fpm/php.ini`) for web uploads. ### PHP Maximum execution time of N seconds exceeded Source: https://rocketeersapp.com/php-maximum-execution-time-exceeded This fatal error means a PHP script ran longer than the allowed time limit and was killed. Raise the limit for legitimately long work, but the real fix is usually to make the script faster or move it to a queue. ## About the error ```bash PHP Fatal error: Maximum execution time of 30 seconds exceeded ``` PHP caps how long a single script may run with the `max_execution_time` setting (default 30 seconds for web requests). When a script exceeds it, PHP terminates it mid-run, which usually surfaces as a [500 error](/500-internal-server-error-laravel) or a half-rendered page. ## Why do I see this error - A slow database query, often a missing index. - Heavy work done inside a web request: large exports, image processing, sending many emails. - A slow or hanging external API call. - An accidental infinite loop. Note that `max_execution_time` counts CPU time of the script itself, not time spent waiting on the database or external calls on all platforms, but on a typical web request the practical effect is a hard ceiling on how long the page can take. ## Solution ### Raise the limit In `php.ini`: ```ini max_execution_time = 120 ``` For a single CLI command without touching config: ```bash php -d max_execution_time=300 artisan some:command ``` Reload PHP-FPM after editing `php.ini`: ```bash systemctl reload php8.3-fpm ``` Remember nginx has its own timeout in front of PHP, if it's lower, nginx gives up first with a [504 Gateway Timeout](/504-gateway-timeout-nginx). Both need to be raised together. ### The real fix: don't do slow work in a request CLI scripts (like `artisan` commands) default to no time limit, so the right home for slow work is a background queue, not the request cycle. In Laravel, dispatch it: ```php ProcessExport::dispatch($user); ``` The request returns instantly and the heavy lifting happens on a worker. Combine that with adding the missing database index and the timeout disappears for good. If the script is dying on memory rather than time, see [PHP allowed memory size exhausted](/php-allowed-memory-size-exhausted). ### PHP Fatal error: Allowed memory size exhausted Source: https://rocketeersapp.com/php-allowed-memory-size-exhausted This fatal error means a PHP script tried to use more memory than the configured limit. Raise the limit, but first ask why the script needs that much. ## About the error The full message looks like this: ```bash PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes) ``` The number `134217728` is the limit in bytes, in this case 128 MB. PHP refused to give the script any more memory and stopped it. Because it is a fatal error, it usually surfaces as a blank page or a [500 error](/500-internal-server-error-laravel) in the browser. ## Why do I see this error PHP caps how much memory a single script may use through the `memory_limit` setting. A script hits that cap for one of two reasons: - It genuinely needs more, for example exporting a large dataset or processing a big file. - It has a problem: loading thousands of Eloquent models at once, an accidental infinite loop, or building a huge array in memory. The amount it "tried to allocate" is a hint. A tiny allocation (like 20 KB above) failing means the script was already sitting right at the limit. ## Solution To raise the limit globally, edit `memory_limit` in your `php.ini`: ```ini memory_limit = 256M ``` For a single CLI command without touching config: ```bash php -d memory_limit=512M artisan some:command ``` And to set it from inside a script at runtime (use sparingly): ```php ini_set('memory_limit', '512M'); ``` After editing `php.ini`, reload PHP-FPM so the change takes effect: ```bash systemctl reload php8.3-fpm ``` Before you reach for a bigger number, check whether the code is the real culprit. In Laravel, the classic cause is loading everything at once: ```php // Loads every row into memory at once $users = User::all(); // Streams rows in small batches instead User::chunk(500, function ($users) { // ... }); ``` Use `chunk()`, `cursor()`, or `lazy()` for large result sets. Setting `memory_limit = -1` (unlimited) hides the problem and risks taking the whole server down, so avoid it in production. ### Composer out of memory (allowed memory size exhausted) Source: https://rocketeersapp.com/composer-out-of-memory-allowed-memory-size-exhausted Composer can run out of memory while resolving dependencies, especially on small VPSes. The real fix is usually swap space, not a bigger memory limit. ## About the error Running `composer update` or `composer require` fails with: ```bash PHP Fatal error: Allowed memory size of 1610612736 bytes exhausted (tried to allocate ...) ``` Dependency resolution is memory-hungry. To pick compatible versions, Composer compares every candidate version of every package against all the others, and large or conflicted dependency trees can exhaust the available memory. ## Why do I see this error - A big dependency tree, or `composer update` resolving everything from scratch. - A small server with little RAM and **no swap** configured. - An old Composer 1.x install, version 1 used far more memory than version 2. ## Solution ### Make sure you're on Composer 2 Composer 2 cut memory use dramatically. Check and upgrade: ```bash composer --version composer self-update --2 ``` ### Add swap space (the proper fix on a small VPS) If `proc_open(): fork failed` or the out-of-memory error shows up on a low-RAM server, the machine simply has no headroom. Adding swap solves it durably without raising PHP limits. See the dedicated guide on [adding swap space on Ubuntu](/add-swap-space-on-ubuntu). ### Raise the memory limit for one command To get unblocked immediately, run Composer with an unlimited limit just for that invocation: ```bash COMPOSER_MEMORY_LIMIT=-1 composer update ``` Or point Composer at a php.ini with a higher limit: ```bash php -d memory_limit=-1 /usr/local/bin/composer update ``` ### Prefer require over update where you can `composer require some/package` resolves a smaller slice of the tree than a full `composer update`, and `composer install` against an existing `composer.lock` barely uses any memory at all. On production you should only ever run `composer install`, never `update`: ```bash composer install --no-dev --optimize-autoloader ``` For installing local packages during development, see [installing Composer packages locally](/install-composer-packages-locally). The same memory ceiling shows up in [increase PHP memory limit](/increase-php-memory-limit). ### PHP Fatal error: Class "X" not found Source: https://rocketeersapp.com/php-class-not-found This fatal error means PHP could not load a class you referenced. Almost always a namespace mismatch, a wrong file name, or an autoloader that needs regenerating. ## About the error ```bash PHP Fatal error: Uncaught Error: Class "App\Services\Invoice" not found ``` PHP tried to use a class and couldn't find a definition for it. With Composer's PSR-4 autoloading, the class name and namespace must map exactly to a file on disk, and any mismatch breaks that lookup. ## Why do I see this error - The **namespace** declared in the file doesn't match its folder. - The **file name** doesn't match the class name (PSR-4 is case-sensitive, and so is Linux). - You forgot a `use` statement, so PHP looks for the class in the current namespace. - The class is new and the autoloader hasn't been regenerated. - A typo in the class name. ## Solution ### Check the namespace matches the path Under PSR-4, `App\Services\Invoice` must live at `app/Services/Invoice.php` and declare: ```php namespace App\Services; class Invoice { // ... } ``` The file name (`Invoice.php`) must match the class name exactly, including case. This works on macOS (case-insensitive filesystem) and then breaks on a Linux server, a very common "works locally, 500 in production" trap. ### Import the class If the class is in another namespace, add a `use` statement at the top of the file: ```php use App\Services\Invoice; $invoice = new Invoice(); ``` ### Regenerate the autoloader If the class genuinely exists and is named correctly, Composer's autoload map may be stale: ```bash composer dump-autoload ``` For framework classes resolved through the container (controllers, etc.), the symptom is slightly different, see [Target class does not exist in Laravel](/target-class-does-not-exist-laravel). ### PHP: Call to a member function on null Source: https://rocketeersapp.com/call-to-a-member-function-on-null This error means you called a method on a variable that turned out to be null. The variable you expected to hold an object was empty, often a database lookup that found nothing. ## About the error ```bash Error: Call to a member function format() on null ``` You called a method (`->format()`, `->name`, `->save()`) on something that was `null` instead of the object you expected. PHP can't call a method on nothing, so it throws. In a web request this reaches visitors as a [500 Internal Server Error](/500-internal-server-error-laravel). The method name in the message is a strong clue: it tells you *which* object was missing. ## Why do I see this error - A database query returned no row, so the model is `null`. - A relationship isn't loaded or doesn't exist for this record. - An optional value (a nullable column, a missing config key) is genuinely empty. - A function you assumed always returns an object returned `null` on failure. ## Solution ### Find the null In Laravel, `find()` and `first()` return `null` when nothing matches. Calling a method on that result fails: ```php $user = User::find($id); // null if no such user echo $user->name; // Call to a member function on null ``` ### Handle the empty case Decide what should happen when it's missing. To 404 automatically, use `findOrFail()`: ```php $user = User::findOrFail($id); // throws a 404 instead of returning null ``` To provide a fallback, check first or use the nullsafe operator (PHP 8+): ```php // guard explicitly if ($user) { echo $user->name; } // or the nullsafe operator: returns null instead of erroring echo $user?->profile?->bio ?? 'No bio'; ``` ### Relationships A relationship that hasn't been set returns `null` too. The nullsafe operator and `optional()` helper both guard against it: ```php $company = $user->company?->name ?? 'Independent'; ``` The fix is rarely to suppress the error, it's to decide deliberately what the empty case should do. A related strictness error is the [undefined array key](/undefined-array-key-php) warning in PHP 8. ### How to run PHP files Source: https://rocketeersapp.com/run-php-files You cannot execute PHP files directly on your computer without any additional tools. You need a (local) webserver that can run the PHP files. ## PHP is a Server-side Language PHP is a language that runs on the server-side, that means it is normally be executed by a (web) server which can return the response that PHP generates. Typically PHP returns HTML, but it can output any type of format or file, like JSON or even images or video. Running it on the server makes PHP also a back-end language. For comparison a great example of a language that is originally not server-side, is Javascript. This is a client-side language that can be used directly in your browser just like HTML, CSS and the likes. It also means that Javascript, CSS and HTML are front-end languages. ## How to run PHP files locally When you want to use PHP locally and in the browser, you actually need a web server running locally on your computer. This way you can use PHP the way it is used most of the time: to build a website or web application. For Windows and macOS users the [XAMPP](https://www.apachefriends.org/) project is still very popular for beginners to easily spin a local web environment to run your PHP application locally using the browser. For macOS users specifically it is highly recommended to use [Laravel Valet](https://laravel.com/docs/valet). This is an CLI tool that installs all the tools you need locally and run your Laravel applications, but even any other type of website or applications like WordPress or Magento. It is a really powerful tool that works by leveraging the power of [Homebrew](https://brew.sh/). ## Command Line When you have PHP installed on your computer or server, you can also use it to execute PHP files using the command line. This is handy when you don't need to output anything visual like a HTML design or graphic content like an image or video. To execute a PHP file you have written, you can run it using this command: ```bash php -r /path/to/php/file ``` When you just want to execute a short command, you do not need to create a separate file for it, you can just run it directly in the command line using the `-r` parameter: ```bash php -r "echo time();" ``` To set PHP up first, see [how to install PHP](/how-to-install-php); for switching versions locally, see [different PHP versions with Laravel Valet](/different-php-versions-laravel-valet). ## Performance ### Tools to validate your website Source: https://rocketeersapp.com/tools-to-validate-website Before and after you launch, a handful of free tools tell you whether your site is secure, fast, and correctly configured. Here are the ones worth running. You can't fix what you can't see. After you've [set up a server](/how-to-host-your-own-website) and put a site live, these free tools grade the things that matter — TLS, security headers, performance, and markup — and tell you exactly what to improve. Run them at launch, then again whenever you make a significant change. ## SSL and TLS ### SSL Labs Server Test The gold standard for inspecting your HTTPS configuration. It grades your TLS setup from A+ down to F and flags weak protocols, missing chain certificates, and poor cipher choices. - **[SSL Labs](https://www.ssllabs.com/ssltest/)** — aim for an A+; you can also run our own [SSL checker](/ssl); here's [how to get there with Cloudflare](/a-plus-grade-ssl-using-cloudflare). A common failure it surfaces is an [incomplete certificate chain](/what-is-an-ssl-certificate-chain). You can also [check expiry yourself from the command line](/check-ssl-certificate-expiration). ## Security headers ### Security Headers and Mozilla Observatory These scan the HTTP response headers your site sends and grade them — whether you set `Strict-Transport-Security`, a Content Security Policy, `X-Content-Type-Options`, and so on. - **[securityheaders.com](https://securityheaders.com)** — quick header grade (or use our [security headers checker](/security-headers)). - **[Mozilla Observatory](https://observatory.mozilla.org)** — a broader security scan. More context in [optimizing web application security](/optimize-web-application-security). ## Performance ### PageSpeed Insights, Lighthouse, and WebPageTest These measure how fast your site loads for real users and give specific, prioritized fixes — render-blocking resources, uncompressed assets, slow server response. - **[PageSpeed Insights](https://pagespeed.web.dev)** — Google's Core Web Vitals report. - **Lighthouse** — built into Chrome DevTools, under the "Lighthouse" tab. - **[WebPageTest](https://www.webpagetest.org)** — detailed waterfall from multiple locations. If your server response is the bottleneck, it'll show up as a slow [time to first byte](/measure-ttfb). General wins are in [optimizing website performance](/optimize-website-performance), and on the server side make sure you've [enabled gzip compression](/enable-gzip-compression-nginx). ## Markup and standards ### W3C validators Catch broken HTML and invalid CSS that can cause subtle rendering and accessibility issues. - **[W3C Markup Validator](https://validator.w3.org)** — HTML. - **[W3C CSS Validator](https://jigsaw.w3.org/css-validator/)** — CSS. ## DNS and email ### DNS and deliverability checkers If you send email from your domain, verify your SPF, DKIM, and DMARC records so your mail isn't flagged as spam. - **[MXToolbox](https://mxtoolbox.com)** — DNS, blacklist, and email record lookups. See [improving email deliverability](/improving-email-deliverability) for what those records should contain. ## Turn results into a launch list Running the tools is step one; acting on them is step two. Pair this with the [website launch checklist](/website-checklist) so nothing slips through before you go live. ## Let Rocketeers handle it Most of what these tools flag — weak TLS, missing security headers, no compression, an expired certificate — comes down to server configuration. Rocketeers provisions servers and sites with strong TLS, sensible headers, and compression on by default, and [monitors your sites](/features/monitor-your-sites-and-servers) after launch, so you spend your time fixing the few things left rather than the basics. ### How to find and optimize slow MySQL queries Source: https://rocketeersapp.com/mysql-query-optimization A repeatable process for MySQL query optimization, find slow MySQL queries with the slow query log, diagnose them with EXPLAIN, and fix them with the right indexes and query rewrites. When a request is slow, the database is usually to blame, and the database is usually slow because of a missing index or a query written in a way the optimizer can't use. MySQL query optimization isn't guesswork: there's a repeatable loop. Find the slow queries with the slow query log, diagnose each one with `EXPLAIN`, then fix it with an index or a rewrite. This guide walks through that loop end to end. ## Find slow queries with the slow query log You can't optimize slow MySQL queries until you know which ones are slow. The slow query log records every statement that takes longer than a threshold you set. Check the current settings: ```sql SHOW VARIABLES LIKE 'slow_query%'; SHOW VARIABLES LIKE 'long_query_time'; ``` Enable it at runtime (no restart needed) and log anything over half a second: ```sql SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 0.5; SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log'; ``` To make it survive a restart, set the same values in `my.cnf` (usually `/etc/mysql/mysql.conf.d/mysqld.cnf`): ```ini [mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 0.5 log_queries_not_using_indexes = 1 ``` `log_queries_not_using_indexes` is worth turning on temporarily, it catches full table scans even when they're currently fast, before the table grows. The raw log is noisy. Use `mysqldumpslow` to aggregate it, so you see the worst offenders first instead of one line per execution: ```bash # Top 10 queries by total time spent mysqldumpslow -s t -t 10 /var/log/mysql/slow.log ``` `-s t` sorts by total time (the queries actually costing you the most), `-t 10` limits to ten results. Start at the top of that list. ## Diagnose with EXPLAIN Once you have a slow query, put `EXPLAIN` in front of it to see how MySQL plans to run it: ```sql EXPLAIN SELECT * FROM orders WHERE customer_id = 42 AND status = 'paid'; ``` Three columns tell you almost everything: - **`type`** — the access method. `ALL` means a **full table scan**, every row read, and is the clearest sign of trouble. `ref`, `eq_ref`, `range`, or `const` mean an index is doing the work. - **`key`** — the index MySQL chose. `NULL` means no index is being used for this query. - **`rows`** — the estimated number of rows MySQL expects to examine. A number close to the table's total row count confirms a scan. So `type = ALL`, `key = NULL`, and a large `rows` estimate together mean the query is scanning the whole table. That's the query to fix next. ## Add the right index Most of the time, the fix for a scanning query is an index on the columns in the `WHERE` clause. For the query above, a composite index covering both filtered columns lets MySQL jump straight to the matching rows: ```sql ALTER TABLE orders ADD INDEX idx_orders_customer_status (customer_id, status); ``` Re-run `EXPLAIN` and confirm `type` is now `ref` and `key` shows your new index. Don't index blindly, though: indexes slow down writes and the column order matters. For when to add a composite index, the leftmost-prefix rule, and the write cost, see the full walkthrough in [how database indexing works](/database-indexing). ## Rewrite index-unfriendly queries An index only helps if the query is written so MySQL can use it. These rewrites fix the most common cases where `key` stays `NULL` even after you add an index. **Don't `SELECT *`.** Selecting only the columns you need keeps result sets small and can let a covering index satisfy the query without touching the table at all: ```sql -- Reads every column, can't be covered by an index SELECT * FROM orders WHERE customer_id = 42; -- Only what you need SELECT id, total, created_at FROM orders WHERE customer_id = 42; ``` **Don't wrap an indexed column in a function.** The index on `created_at` is useless here because MySQL has to compute `DATE()` for every row: ```sql -- Can't use the index SELECT id FROM orders WHERE DATE(created_at) = '2026-06-23'; -- Range condition, uses the index SELECT id FROM orders WHERE created_at >= '2026-06-23' AND created_at < '2026-06-24'; ``` **Avoid a leading wildcard in `LIKE`.** `'%smith'` forces a scan; `'smith%'` can use the index because the prefix is fixed: ```sql SELECT id FROM customers WHERE name LIKE 'smith%'; ``` **Prefer range conditions and always limit large result sets.** Returning 100,000 rows to the application is slow regardless of indexing, page the results: ```sql SELECT id, total FROM orders WHERE customer_id = 42 ORDER BY created_at DESC LIMIT 50; ``` ## EXPLAIN ANALYZE on MySQL 8.0 `EXPLAIN` shows the optimizer's *plan*. On MySQL 8.0, `EXPLAIN ANALYZE` actually *runs* the query and reports real timings per step, so you can see where the time genuinely goes rather than relying on estimates: ```sql EXPLAIN ANALYZE SELECT id, total FROM orders WHERE customer_id = 42; ``` If you're still on 5.7 you won't have this, and you're missing optimizer improvements and better index limits too, see [upgrading MySQL 5.7 to 8.0 on Ubuntu](/upgrade-mysql-5-7-to-8-0-ubuntu). Server-wide tuning (buffer pool size, connection limits) is a separate lever covered in [optimizing MySQL performance](/optimize-mysql-performance). And if a query won't run at all, the cause is often a [1064 SQL syntax error](/mysql-1064-sql-syntax-error). ## Conclusion MySQL query optimization is a loop, not a one-time fix: enable the slow query log to find the worst queries, run `EXPLAIN` to see why they're slow, then add a focused index or rewrite the query so the index can be used. Confirm every change with `EXPLAIN` before moving on, and keep watching the slow log as your data grows, the query that's fast today on ten thousand rows is the full table scan that pages you at ten million. ### How to clear the Redis cache Source: https://rocketeersapp.com/clear-redis-cache A practical guide to clear the Redis cache, from flushing a single database with redis-cli to safely deleting keys by pattern and clearing the cache from Laravel. You usually clear the Redis cache after a deploy, when stale data is being served, or while debugging locally. The commands are simple, but on a shared or production instance they are destructive: a single flush wipes every cached value at once. This guide covers how to clear the Redis cache safely, from a full flush down to deleting individual keys. ## FLUSHDB vs FLUSHALL Redis splits its keyspace into numbered databases (0 by default). The two flush commands differ in scope: - **`FLUSHDB`** clears the *current* database only. - **`FLUSHALL`** clears *every* database on the instance. Connect with `redis-cli` and pick a database with `-n`: ```bash # Clear database 0 (the default) redis-cli FLUSHDB # Connect to database 2, then clear it redis-cli -n 2 FLUSHDB # Wipe every database on the instance redis-cli FLUSHALL ``` Inside an interactive session you select the database with `SELECT`: ```bash redis-cli 127.0.0.1:6379> SELECT 2 OK 127.0.0.1:6379[2]> FLUSHDB OK ``` If `redis-cli` itself can't connect, that's a separate problem, see [Redis connection refused in Laravel](/redis-connection-refused-laravel). ## Clear specific keys Flushing is rarely what you want in production. To remove one key, use `DEL`: ```bash redis-cli DEL session:abc123 redis-cli DEL user:42 user:43 user:44 ``` To delete everything matching a pattern, you might reach for `KEYS`, **don't**. `KEYS` scans the entire keyspace in a single blocking operation; on a large dataset it freezes the server for the duration. Use `SCAN`, which iterates in small batches, and pipe the results into `DEL`: ```bash redis-cli --scan --pattern 'session:*' | xargs -L 100 redis-cli DEL ``` `--scan` cursors through the keyspace without blocking, and `-L 100` deletes in chunks of 100 keys so you don't build one enormous command. This is the safe way to clear the Redis cache for a subset of keys on a live instance. ## Async (non-blocking) flush `FLUSHDB` and `FLUSHALL` are synchronous by default: on a cache holding millions of keys, freeing that memory blocks the server. The `ASYNC` modifier hands the cleanup to a background thread so the command returns immediately: ```bash redis-cli FLUSHDB ASYNC redis-cli FLUSHALL ASYNC ``` Use `ASYNC` on any large production cache. The keys disappear right away; only the memory reclamation happens in the background. ## Clearing the cache from Laravel If Redis is your Laravel cache store, you rarely touch `redis-cli` at all. The Artisan command clears the default store: ```bash php artisan cache:clear ``` The programmatic equivalent is `Cache::flush()`: ```php use Illuminate\Support\Facades\Cache; Cache::flush(); ``` To clear a specific store instead of the default, name it: ```php Cache::store('redis')->flush(); ``` ```bash php artisan cache:clear --store=redis ``` One caveat: `flush()` clears the *entire* store, including any non-cache data sharing that Redis database (sessions, queues). If you only want to drop tagged entries, flush by tag: ```php Cache::tags(['users'])->flush(); ``` For the full set of Laravel options see [clearing the cache in Laravel](/clear-cache-laravel) and the [Laravel cache](/laravel-cache) reference. ## A word of caution in production Clearing the cache in production isn't free. When you flush, every subsequent request misses the cache at once and falls through to the database. That sudden surge, a **cache stampede** or **thundering herd**, can overload the database hard enough to take the site down, exactly when you were trying to fix it. To soften the impact: - Clear specific keys with `SCAN` + `DEL` instead of a full flush whenever you can. - Warm critical keys right after flushing. - Use `FLUSHALL ASYNC` so the flush itself doesn't block Redis. ## Conclusion To clear the Redis cache, match the tool to the blast radius: `DEL` or `SCAN` for individual keys, `FLUSHDB` for one database, `FLUSHALL` for the whole instance, and `ASYNC` on anything large. From Laravel, prefer `php artisan cache:clear` or `Cache::flush()`. In production, lean toward targeted deletes and be ready for the cache stampede a full flush can trigger. ### A complete guide to caching in Laravel Source: https://rocketeersapp.com/laravel-cache The Laravel cache gives you one API in front of file, Redis, database, and memcached stores. Here is how to configure drivers, cache slow queries, invalidate by tag, and clear it all. Caching is the cheapest performance win you have after indexing. The Laravel cache wraps file, database, Redis, and memcached behind a single `Cache` facade, so you write the same code regardless of where the data lives. This guide covers the drivers, the day-to-day API, cache tags, and how application caching differs from data caching. ## What the Laravel cache is for Laravel caching stores the result of expensive work, a slow query, an API call, a rendered fragment, so you compute it once and serve it from fast storage afterwards. Everything goes through the unified `Illuminate\Support\Facades\Cache` facade, which talks to whichever **store** you've configured. Swap the store from `file` to `redis` and not a line of your application code changes. ## Cache drivers Laravel ships with several drivers, configured in `config/cache.php` and selected per environment in `.env`: - **`file`** — serializes values to `storage/framework/cache`. Zero setup, fine for a single small server. Slow and not shared across machines. - **`database`** — stores cache rows in a table. Survives deploys, works across servers, but adds load to the DB you're usually trying to protect. - **`redis`** — in-memory, fast, supports tags and atomic locks. The default choice for production. - **`memcached`** — also in-memory and fast; supports tags but lacks Redis's richer data types and persistence. - **`array`** — keeps values in PHP memory for the current request only. Used in tests. Select the store in `.env`. Newer Laravel (11+) uses `CACHE_STORE`; older releases use `CACHE_DRIVER`: ```ini # Laravel 11 and newer CACHE_STORE=redis # Laravel 10 and older CACHE_DRIVER=redis REDIS_HOST=127.0.0.1 REDIS_PORT=6379 ``` For production, use Redis. It's fast, shared across web nodes and queue workers, and unlocks tags and locks. If your app can't reach the server, see [Redis connection refused in Laravel](/redis-connection-refused-laravel). The `database` driver was the default in older skeletons; it works but doesn't scale as well under read pressure. ## Basic usage The facade exposes a small, predictable API. ```php use Illuminate\Support\Facades\Cache; Cache::put('key', 'value', now()->addMinutes(10)); // store with TTL Cache::get('key', 'default'); // read, with fallback Cache::has('key'); // existence check Cache::forever('key', 'value'); // no expiry Cache::forget('key'); // delete one key ``` The method you'll reach for most is `remember()`. It returns the cached value if present, otherwise runs the closure, stores the result, and returns it, so the slow path runs only on a miss: ```php $users = Cache::remember('users.active', now()->addHour(), function () { return User::where('active', true) ->withCount('orders') ->get(); }); ``` That single call replaces the read/compute/write dance and is the canonical way to wrap a slow query. Use `rememberForever()` for values that never expire on a clock and are invalidated explicitly instead. ## Cache tags for grouped invalidation When several keys belong together, **tags** let you invalidate them as a group instead of tracking every key by hand. Tags are only supported on the `redis` and `memcached` drivers, not `file` or `database`. ```php Cache::tags(['users', 'billing'])->put('user.42.invoices', $invoices, 3600); // Later, blow away everything tagged "users" Cache::tags(['users'])->flush(); ``` This is ideal for per-model caches: tag every entry for a user with `user.{id}`, then flush that one tag when the user changes, leaving the rest of the cache untouched. ## Application caching vs data caching There's a second kind of caching in Laravel that has nothing to do with the `Cache` facade: **application/config caching**. These Artisan commands compile framework files into a single fast-loading file and matter most in production. ```bash php artisan config:cache # merge all config into one cached file php artisan route:cache # compile route definitions php artisan view:cache # precompile Blade templates ``` Run these on deploy. The catch with `config:cache` is that `env()` calls outside of config files return `null` once config is cached, so read environment values through `config()` only. These caches are about boot speed; the `Cache` facade is about your data. They are independent systems with independent clear commands. For the bigger picture on tuning a production app, see the [Laravel performance guide](/laravel-performance), and pair the config caches with [OPcache enabled in PHP](/enable-opcache-php) for the largest boot-time gains. ## Clearing the cache Data cache and application caches clear separately: ```bash php artisan cache:clear # flush the data cache (Cache facade) php artisan config:clear # drop cached config php artisan route:clear # drop cached routes php artisan view:clear # drop compiled views php artisan optimize:clear # all of the above at once ``` For the full rundown of when and why to run each, see [clearing the cache in Laravel](/clear-cache-laravel). If you're on Redis specifically and need to flush at the store level, see [clearing the Redis cache](/clear-redis-cache). ## Choosing a driver A quick decision guide: - **Local / tiny single server:** `file` — no dependencies, good enough. - **Tests:** `array` — isolated per request, nothing to clean up. - **Multi-server, no Redis yet:** `database` — shared and persistent, at some DB cost. - **Production:** `redis` — fast, shared, supports tags and locks. The recommended default. If you need cache tags, you must be on `redis` or `memcached`; `file` and `database` silently don't support them. ## Conclusion The Laravel cache gives you one API over many backends: configure the store in `.env`, wrap slow work in `Cache::remember()`, group related keys with tags for clean invalidation, and keep application/config caching separate in your deploy step. Use Redis in production, and when something looks stale, reach for `cache:clear` or `optimize:clear` before you start debugging. If you run the memcached driver, you'll first need to [install the PHP memcached extension](/install-php-memcached-extension-on-macos). ### How to enable and configure OPcache for faster PHP Source: https://rocketeersapp.com/enable-opcache-php OPcache caches compiled PHP bytecode so your code skips recompilation on every request. Here is how to enable and tune OPcache for a real production server. OPcache is one of the biggest speedups available to a PHP app, and it costs you almost nothing to turn on. Every time PHP runs a script it normally reads the file, parses it, and compiles it to bytecode before executing. OPcache stores that compiled bytecode in shared memory, so subsequent requests skip the parse-and-compile step entirely. On a typical Laravel or WordPress app that alone can cut response times by a large margin. ## What OPcache does PHP is interpreted, but it doesn't run your source directly. It compiles each `.php` file into intermediate bytecode (opcodes) and then executes that. Without a cache, this compilation happens on *every single request*, for every file the request touches. OPcache caches the compiled opcodes in shared memory the first time a file runs. After that, PHP fetches the bytecode straight from memory and goes directly to execution. The parsing and compilation overhead disappears, which is why enabling OPcache is usually the first PHP performance change worth making. ## Check whether OPcache is enabled OPcache ships with PHP and is often already installed, just not configured well. Check from the CLI: ```bash php -i | grep opcache.enable ``` A clearer view comes from `opcache_get_status()`, which reports live memory usage and hit rate. Note this reflects the CLI SAPI when run from the command line; for FPM, expose it through a web script: ```php author->name; // queries the authors table every iteration } ``` Eager load the relationship with `with()` so it's fetched in a single extra query: ```php // 2 queries total, no matter how many posts $posts = Post::with('author')->get(); foreach ($posts as $post) { echo $post->author->name; // already loaded } ``` You can catch these automatically in development with `Model::preventLazyLoading()` in a service provider, which throws when a relationship is lazy-loaded. Eager loading reduces the *number* of queries. The speed of each query is a database concern: make sure the foreign-key columns you join on are indexed, or every query still does a full table scan. See [how database indexing works](/database-indexing) for the underlying DB side. ## Offload slow work to queues Sending email, processing images, calling third-party APIs, generating PDFs, none of it needs to happen inside the request. Push it onto a queue so the user gets an instant response and the work runs in the background. ```php // Instead of sending inline Mail::to($user)->send(new WelcomeEmail($user)); // Queue it Mail::to($user)->queue(new WelcomeEmail($user)); ``` Any job can be queued by dispatching it: ```php ProcessPodcast::dispatch($podcast); ``` Run a worker to process the queue, and keep it alive with a process manager like Supervisor: ```bash php artisan queue:work --tries=3 ``` This moves latency out of the request path, which is often the difference between a 1.5-second page and a 150-millisecond one. ## Use a fast cache and session driver The default `file` driver writes cache and session data to disk, which is slow and doesn't scale across multiple servers. Switch to Redis for both. ```ini CACHE_STORE=redis SESSION_DRIVER=redis QUEUE_CONNECTION=redis ``` Redis is an in-memory store, so reads and writes are sub-millisecond, and it works as a shared backend when you scale to more than one app server. See [using Redis for the Laravel cache](/laravel-cache) for setup and patterns. If your worker can't reach it, check [Redis connection refused in Laravel](/redis-connection-refused-laravel). ## Enable OPcache on the server PHP recompiles your scripts to bytecode on every request unless OPcache is enabled. OPcache keeps the compiled bytecode in memory, eliminating that work entirely. It's the highest-impact server-side change you can make and applies to any PHP app. See [enabling OPcache in PHP](/enable-opcache-php) for the recommended `php.ini` settings, and [general PHP performance tuning](/php-performance) for more. ## Speed up the frontend Backend speed is only half the page. Bundle and minify your assets with Vite, which ships with modern Laravel: ```bash npm run build ``` This produces hashed, minified, tree-shaken bundles and lets the browser cache them aggressively. On the response side, cache rendered output for pages that don't change per-user (marketing pages, docs) and lean on HTTP caching headers so repeat visits skip the server entirely. ## Conclusion Laravel performance comes down to a short checklist: run `php artisan optimize` and clear it on deploy, build the autoloader with `--optimize-autoloader --no-dev`, eliminate N+1 queries with `with()`, push slow work to queues, use Redis for cache and sessions, and enable OPcache. Do these and a stock Laravel app handles serious traffic before you ever need to think about the framework itself. On a queue-heavy box you can also tune [Horizon's process priority](/laravel-horizon-nice-process-priority); and if you're unsure which release you're on, [check your Laravel version](/check-laravel-version). ### How to optimize MySQL performance Source: https://rocketeersapp.com/optimize-mysql-performance A practical guide to MySQL performance tuning that starts with measurement, then covers indexes, the InnoDB buffer pool, connections, and server resources. MySQL performance tuning is measure-then-change, not copy-pasting a `my.cnf` you found in a forum. Random config tweaks usually do nothing, and occasionally make things worse. The reliable way to optimize MySQL performance is to find what's actually slow, fix that one thing, and confirm it helped before moving on. This guide walks through the levers that matter, roughly in the order you should reach for them. ## Measure first Before you touch a single setting, find out what's slow. Turn on the slow query log and let it collect real traffic: ```ini [mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1 log_queries_not_using_indexes = 1 ``` `long_query_time = 1` logs anything over one second; lower it once the obvious offenders are gone. After a day of traffic, summarise the log with `mysqldumpslow` or `pt-query-digest` to see which queries cost the most total time. The queries at the top of that list are where your tuning effort belongs. Rewriting one bad query usually beats any config change. See [MySQL query optimization](/mysql-query-optimization) for how to read `EXPLAIN` and fix the offenders. ## Add proper indexes The single most common cause of a slow query is a missing index. A query that filters or joins on an unindexed column forces a full table scan, which scales linearly with table size: fine at a thousand rows, painful at a million. ```sql EXPLAIN SELECT * FROM orders WHERE user_id = 42 AND status = 'paid'; ``` If `EXPLAIN` shows `type: ALL` and `key: NULL`, you're scanning. Add an index that covers the filtered columns: ```sql ALTER TABLE orders ADD INDEX idx_orders_user_status (user_id, status); ``` Indexing is deep enough to deserve its own treatment, including composite indexes, the leftmost-prefix rule, and what *not* to index. See [how database indexing works](/database-indexing). ## Size the InnoDB buffer pool After indexes, the buffer pool is the highest-impact setting. `innodb_buffer_pool_size` is the in-memory cache for table and index data; when your working set fits in it, reads come from RAM instead of disk. On a **dedicated** database server, set it to roughly **50-70% of total RAM**, leaving headroom for the OS, connections, and per-query buffers: ```ini [mysqld] innodb_buffer_pool_size = 6G innodb_buffer_pool_instances = 4 ``` To pick a number, first check how much RAM the machine has, see [how much memory is on Ubuntu](/how-much-memory-on-ubuntu). On an 8 GB dedicated box, `6G` is reasonable. On a shared box that also runs PHP-FPM and Nginx, be more conservative, those processes need memory too, and pushing MySQL too high causes swapping. The default is only 128 MB, so this is almost always worth changing. Verify it took effect: ```sql SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; ``` ## Connections and "Too many connections" `max_connections` caps how many clients can connect at once. The default of 151 is fine for many apps, but a traffic spike or a connection leak can exhaust it, and new clients get **ERROR 1040: Too many connections**. ```ini [mysqld] max_connections = 300 ``` Resist the urge to set this to thousands. Each connection consumes memory, and a high cap can let a runaway app pile up connections until the server runs out of RAM. The real fix is usually shorter-lived connections, a sane pool size in the app, or persistent connections, not a bigger number. See [MySQL 1040: too many connections](/mysql-1040-too-many-connections) for diagnosis and the proper fix. ## The query cache is gone in MySQL 8.0 If an old tuning guide tells you to set `query_cache_size`, ignore it. The query cache was **removed in MySQL 8.0**, it was a global-lock bottleneck that hurt throughput on write-heavy workloads. Don't try to enable it; the variables no longer exist and MySQL won't start if you reference them. Cache in the application layer instead. Redis or Memcached in front of expensive read queries gives you far more control and none of the contention. ## Temp tables, sort buffers, and logging A few per-session buffers help specific workloads, but only raise them when the slow log points there: ```ini [mysqld] tmp_table_size = 64M max_heap_table_size = 64M sort_buffer_size = 4M ``` `tmp_table_size` and `max_heap_table_size` together decide when an in-memory temp table spills to disk; raise both (they must match) if you see `created_tmp_disk_tables` climbing in `SHOW GLOBAL STATUS`. Keep `sort_buffer_size` modest, it's allocated per connection, so a large value multiplied by many connections eats memory fast. Leave the slow query log on in production at a sensible `long_query_time` so regressions surface on their own. ## Server resources No config can rescue a starved server. MySQL wants RAM, and it must not swap, going to disk for memory turns millisecond queries into multi-second ones. If the box is borderline, add a swap file as a safety valve against the OOM killer, but treat real swapping as a signal to add RAM or shrink the buffer pool. See [add swap space on Ubuntu](/add-swap-space-on-ubuntu). Storage matters too: use SSDs, always. And if you're still on MySQL 5.7, upgrading to 8.0 is one of the biggest free wins available, a smarter optimizer, better defaults, and improved indexing limits. See [upgrading MySQL 5.7 to 8.0 on Ubuntu](/upgrade-mysql-5-7-to-8-0-ubuntu). ## Conclusion Optimizing MySQL performance is a loop, not a one-time config dump: measure with the slow query log, fix the worst query (usually with an index), size the buffer pool to your RAM, keep connections sane, and make sure the server has memory to spare without swapping. Change one thing at a time and confirm it helped. That discipline beats any "ultimate `my.cnf`" you'll find online. ### How database indexing works (with MySQL examples) Source: https://rocketeersapp.com/database-indexing An index is the single biggest lever you have on query speed. Here is what a database index actually is, how it works under the hood, and how to add the right ones in MySQL. When a query gets slow, the cause is almost always a missing index. Indexing is the highest-impact thing you can do for database performance, yet it stays a little mysterious. This guide explains what an index is, how it speeds up reads, and how to add and verify indexes in MySQL. ## What is a database index An index is a separate, sorted data structure that lets the database find rows without scanning the whole table. The classic analogy is the index at the back of a book. To find every mention of "InnoDB", you don't read all 400 pages, you flip to the index, jump to "I", and get the exact page numbers. A database index does the same thing for your rows. Without an index, MySQL has to do a **full table scan**, reading every row to find the ones that match. On a few hundred rows that's instant. On a few million it's the difference between 2 milliseconds and 2 seconds. ## How indexes work under the hood Most MySQL indexes (everything in InnoDB by default) are stored as a **B-tree**: a balanced tree that keeps values in sorted order and stays shallow even for huge tables. Because the tree is sorted and balanced, MySQL finds any value in a handful of steps instead of a linear scan. A table with a million rows is only a few levels deep, so a lookup touches a handful of nodes rather than a million rows. That same sorted structure is also why an index can satisfy range conditions (`>`, `<`, `BETWEEN`) and `ORDER BY` without sorting afterwards. The trade-off: the tree has to stay sorted, so every `INSERT`, `UPDATE`, and `DELETE` has to update every affected index too. More on that cost below. ## Creating an index in MySQL Say you frequently look up users by email: ```sql SELECT * FROM users WHERE email = 'jane@example.com'; ``` If `email` isn't indexed, that's a full table scan. Add an index: ```sql CREATE INDEX idx_users_email ON users (email); ``` Or while creating/altering the table: ```sql ALTER TABLE users ADD INDEX idx_users_email (email); ``` If the column should be unique (like an email), use a unique index instead, which enforces uniqueness *and* speeds up lookups: ```sql ALTER TABLE users ADD UNIQUE INDEX idx_users_email (email); ``` In a Laravel migration the equivalent is: ```php $table->string('email')->unique(); // unique index $table->index('last_login_at'); // plain index ``` ## Verify the index is actually used Adding an index is only half the job, you need to confirm MySQL uses it. Put `EXPLAIN` in front of your query: ```sql EXPLAIN SELECT * FROM users WHERE email = 'jane@example.com'; ``` Look at two columns: - **`type`** — `ALL` means a full table scan (bad). `ref`, `eq_ref`, or `const` means an index is being used (good). - **`key`** — the index MySQL chose. `NULL` here means no index was used. If `key` is `NULL` after you added an index, the query usually isn't written in an index-friendly way (see common mistakes below). ## What to index Index the columns that appear in: - **`WHERE` filters** — `WHERE status = 'active'` - **`JOIN` conditions** — the foreign-key columns on both sides - **`ORDER BY` / `GROUP BY`** — an index can return rows already sorted, skipping a separate sort step Foreign keys are a common blind spot. A column like `posts.user_id` used in joins should almost always be indexed, otherwise every join does a scan. ## Composite indexes and the leftmost-prefix rule When a query filters on several columns together, a **composite index** beats several single-column ones: ```sql ALTER TABLE orders ADD INDEX idx_orders_user_status (user_id, status); ``` This index helps queries that filter on `user_id`, or on `user_id` **and** `status` together: ```sql SELECT * FROM orders WHERE user_id = 42 AND status = 'paid'; ``` The catch is the **leftmost-prefix rule**: MySQL can only use the index left-to-right. The index above helps `user_id` alone, and `user_id + status`, but **not** `status` alone, because `status` isn't the leftmost column. Order the columns by how you actually query them. ## The cost of indexes Indexes are not free, so don't index every column: - **Slower writes** — every `INSERT`/`UPDATE`/`DELETE` must update each index. Over-indexing a write-heavy table hurts. - **Disk and memory** — indexes take space and compete for the InnoDB buffer pool. - **Maintenance** — redundant indexes (e.g. an index on `(a)` when you already have `(a, b)`) waste resources for no gain. A good rule of thumb: add indexes to support your real, slow queries, then remove ones that `EXPLAIN` never chooses. ## Common mistakes - **Wrapping the column in a function** — `WHERE DATE(created_at) = '2026-06-23'` can't use an index on `created_at`. Rewrite as a range: `WHERE created_at >= '2026-06-23' AND created_at < '2026-06-24'`. - **Leading wildcards** — `WHERE name LIKE '%smith'` can't use an index; `LIKE 'smith%'` can. - **Indexing low-cardinality columns** — an index on a boolean or a `status` with two values rarely helps on its own. - **Hitting the key-length limit** — very long `VARCHAR` indexes can fail on older MySQL. See [MySQL 1071: specified key was too long](/mysql-1071-specified-key-was-too-long). ## Conclusion Indexing is the first place to look when a query is slow. Find the columns in your `WHERE`, `JOIN`, and `ORDER BY` clauses, add a focused index, and confirm with `EXPLAIN` that MySQL uses it, while keeping an eye on write cost so you don't over-index. A unique index is also what raises [duplicate entry (1062)](/sqlstate-23000-1062-duplicate-entry) when a clashing row is inserted. If you're still on an older MySQL release, upgrading also unlocks better indexing limits and a smarter optimizer, see [upgrading MySQL 5.7 to 8.0 on Ubuntu](/upgrade-mysql-5-7-to-8-0-ubuntu). ### How to increase the PHP memory limit Source: https://rocketeersapp.com/increase-php-memory-limit The PHP memory limit caps how much memory a single script can use. Here is how to check it, find the right php.ini, and raise it safely for CLI, FPM, and per-app cases. `memory_limit` is the maximum amount of memory a single PHP script is allowed to allocate. When a script exceeds it, PHP kills the request with a fatal error rather than letting it eat the whole server. You usually meet the PHP memory limit the hard way, through the dreaded [allowed memory size exhausted](/php-allowed-memory-size-exhausted) error: ```bash PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes) ``` That `134217728` is 128M, the common default. The fix is to raise the limit, but only in the right place. This guide shows where that place is. ## Check the current limit From the CLI, the quickest check: ```bash php -i | grep memory_limit ``` Inside a request, drop a `phpinfo()` call or read the value directly: ```php echo ini_get('memory_limit'); // e.g. "128M" ``` Note that the CLI value and the web (FPM) value are often different, so check both, not just whichever is convenient. ## Find the right php.ini This is the step that trips people up. The PHP CLI and PHP-FPM load **different** configuration files. Editing the CLI's `php.ini` will do nothing for your web requests, and vice versa. Ask PHP itself which file it uses: ```bash php --ini ``` ```text Configuration File (php.ini) Path: /etc/php/8.3/cli Loaded Configuration File: /etc/php/8.3/cli/php.ini ``` That `cli` path is for command-line scripts. The web server uses the FPM file, typically `/etc/php/8.3/fpm/php.ini`. Confirm the FPM path from a `phpinfo()` page served through your web server. ## Set memory_limit in php.ini Open the correct file and set the value: ```ini memory_limit = 256M ``` Use a plain integer plus a unit suffix: `K`, `M`, or `G`. A value of `-1` means **unlimited**, which is discouraged on a web server, because a single runaway request can starve the whole machine. Pick a real ceiling instead. Reload so the change takes effect. For the CLI, no reload is needed, the next `php` invocation picks it up. For FPM, restart the service: ```bash sudo systemctl restart php8.3-fpm ``` ## Per PHP-FPM pool override You often want a higher limit for one app, not every site on the box. PHP-FPM pools let you override `memory_limit` per pool, in the pool config (e.g. `/etc/php/8.3/fpm/pool.d/www.conf`): ```ini php_admin_value[memory_limit] = 512M ``` `php_admin_value` sets the limit and prevents the application from lowering or overriding it at runtime. After editing the pool, restart FPM: ```bash sudo systemctl restart php8.3-fpm ``` This is the cleanest way to give a memory-hungry app more headroom while keeping the global default conservative. While you are in the pool config, it is worth [disabling unused PHP-FPM pools](/disable-unused-php-fpm-pools) so they aren't holding resources. ## Per-app and per-directory overrides If you can't touch the global config, raise the PHP memory limit closer to the application. **`.user.ini`** — drop a file in your app's web root (works with PHP-FPM/CGI): ```ini memory_limit = 256M ``` Changes are cached and picked up after `user_ini.cache_ttl` (300 seconds by default), so they aren't instant. **`ini_set()` at runtime** — raise it for a single script before the heavy work starts: ```php ini_set('memory_limit', '512M'); ``` This fails if the limit was locked with `php_admin_value`, and it can't help a script that runs out of memory before this line executes. **`.htaccess`** — on Apache with `mod_php`: ```apache php_value memory_limit 256M ``` This does nothing under PHP-FPM, which ignores `.htaccess` PHP directives. ## Apply it only where needed Resist the urge to set a huge limit globally. A high web limit lets one bad request consume gigabytes; `-1` everywhere removes the safety net entirely. Raise the limit for the specific pool, directory, or script that needs it, and leave the global default sane. Remember the CLI/web split. Long-running command-line jobs legitimately need more memory than web requests, which is why Composer in particular hits the wall, see [Composer out of memory: allowed memory size exhausted](/composer-out-of-memory-allowed-memory-size-exhausted). Because the CLI uses its own `php.ini`, you can give it a generous limit (or `-1`) without loosening anything your web traffic touches. ## Conclusion Raising the PHP memory limit is straightforward once you know which `php.ini` is in play: confirm with `php --ini`, set `memory_limit` in the right file, and restart FPM for web changes. Override per pool, per directory, or per script when only one app needs more, and keep the global default modest so a single request can't take down the server. If you're tuning memory because pages are slow rather than crashing, that's a different problem, start with [PHP performance](/php-performance). ### Brotli vs Gzip: which compression should you use? Source: https://rocketeersapp.com/brotli-vs-gzip Brotli vs Gzip is the practical choice for compressing text responses. Here is how the two differ on ratio, speed, and support, and which one to serve. Both Brotli and Gzip do the same job: they shrink text responses (HTML, CSS, JS, JSON) before sending them over the wire so pages load faster on the client. The Brotli vs Gzip question isn't really "which is better" in the abstract, it's which one to use for which kind of asset. This guide covers the practical differences in ratio, speed, and browser support, then gives a recommendation you can act on. ## What Gzip is Gzip is the long-standing standard for HTTP compression. It's built on the **DEFLATE** algorithm (LZ77 plus Huffman coding) and has been supported by every browser and server worth caring about for two decades. Its strengths are universal support and speed. Gzip compresses quickly even at higher levels, so it's safe to use on dynamic responses generated per request. It's the safe default that works everywhere. ## What Brotli is Brotli is a newer algorithm developed at Google, designed specifically with the web in mind. It uses similar techniques to DEFLATE but adds a few things that pay off on text: larger compression windows, context modelling, and a **built-in dictionary** of common words and HTML/CSS/JS fragments. That built-in dictionary is the key difference. Because web responses share a lot of boilerplate, Brotli can reference common strings without spending bytes describing them, which is why it tends to beat Gzip on exactly the content most sites serve. ## Compression ratio On text, Brotli generally produces smaller output than Gzip at comparable settings, often in the rough range of **15-25% smaller** for HTML, CSS, and JS. The exact savings depend heavily on the file and the levels you compare, so treat that as a ballpark rather than a guarantee. The gain is largest on text-heavy assets. For already-compressed binaries (images, fonts in WOFF2, video) neither algorithm helps much, so don't bother compressing those. ## Speed and levels This is where the choice actually gets decided. - **Gzip** runs at levels **1-9**. Higher means smaller but slower; the middle levels are a good balance for on-the-fly compression. - **Brotli** runs at levels **0-11**. The top level (11) squeezes out the best ratio but is **slow to compress** — far slower than Gzip. That speed cost shapes how you use each one: | Scenario | Best choice | | --- | --- | | Static assets (CSS/JS bundles) | Brotli at level 11, precompressed at build time | | Dynamic responses (HTML, API JSON) | Brotli at a low level, or Gzip | | Maximum compatibility | Gzip as the fallback | The insight: Brotli 11 is expensive to run once but cheap to serve forever, so it's ideal for **static, precompressed assets**. For dynamic responses you pay the cost on every request, so a lower Brotli level or plain Gzip is the better trade. ## Browser support Both are effectively universal in modern browsers. Gzip works everywhere. Brotli is supported by all current browsers, with the one caveat that they only advertise it **over HTTPS**. The browser tells the server what it accepts in the `Accept-Encoding` request header: ```http Accept-Encoding: br, gzip ``` `br` is Brotli. The server picks the best encoding it supports from that list and signals its choice back in the `Content-Encoding` response header. If a client only sends `gzip`, you fall back to Gzip automatically. ## Recommendation You don't have to pick one. Serve **Brotli when the client supports it, with Gzip as the fallback** — this is how a well-configured server already behaves based on `Accept-Encoding`. The one decision worth making deliberately: - **Static assets** (your built CSS/JS): precompress them at build time with Brotli at level 11. The server then serves the `.br` file directly with no per-request cost. - **Dynamic responses** (HTML, JSON APIs): use a low Brotli level or Gzip so compression doesn't add latency to [time to first byte](/measure-ttfb). Compression is one of the highest-leverage things you can do for [overall site performance](/optimize-website-performance), and it costs almost nothing to turn on. ## How to enable it On Nginx, both algorithms are configured in a few lines. The Gzip side is covered step by step in [enable Gzip compression on Nginx](/enable-gzip-compression-nginx); Brotli works the same way once the `ngx_brotli` module is loaded, with directives for both dynamic compression and serving precompressed `.br` files. ## Conclusion In the Brotli vs Gzip comparison there's no real loser. Brotli wins on ratio for text and is the right choice for precompressed static assets at level 11. Gzip is the universal, fast fallback that's hard to go wrong with. Configure your server to prefer Brotli and fall back to Gzip, precompress your static bundles, and skip compressing already-compressed binaries. ### How to measure TTFB (Time To First Byte) Source: https://rocketeersapp.com/measure-ttfb TTFB is one of the most important metrics to measure your website or webapp performance. Because only after the first byte the browser can begin to render it. The performance of a web server is very important for your web application. The first metric that comes to mind when measuring web application performance is the TTFB (Time To First Byte). This number shows the time it takes for the server to return the first byte in response of a request from a client. We can easily get the time it takes for the server to process a HTTP request using the following command: ```bash URL="https://rocketee.rs" curl -o /dev/null \ -H 'Cache-Control: no-cache' \ -s \ -w "TTFB: %{time_starttransfer}" \ $URL ``` This command takes care of two important things: 1. Make sure we don't get an earlier cached version of the webpage 2. Only shows the number we're interested in using silent mode and directing output to `/dev/null` ### How to optimize website performance Source: https://rocketeersapp.com/optimize-website-performance Learn how to improve your website or web app performance by leveraging different techniques that optimize the loading speed drastically for your users. **This article will be continuously updated with new content.** ### How to optimize server performance Source: https://rocketeersapp.com/optimize-server-performance Server performance is determinded by a lot of moving parts inside your server. Lets dig in on every aspect that can make a big difference. **This article will be continuously updated with new content.** ## Disable unnecessary and unused PHP versions (FPM pools) When running PHP applications or websites it's a common mistake to keep unused PHP versions running on your server. Mostly this happens when you upgrade the default PHP server version or add a new PHP version to run your application on. In the background these processes do not much harm, but they always will be occupying precious server memory. [Disable unnecessary and unused PHP versions (FPM pools)](/disable-unused-php-fpm-pools) ## Make sure enough diskspace is available When a server does not have enough (a few GB's) of diskspace available, it cannot run within optimal conditions. Because of this, as a treshold make sure there is more than 20% available of the total diskspace capacity. To make sure we have enough diskspace available, there are some commands that can help you with this. [Reclaim diskspace on Ubuntu server](/reclaim-diskspace-on-ubuntu) ## Add Swap Space to your server To increase performance you need to make sure your server has enough memory to make sure it can execute every task. While swap space is slower than usual RAM memory, it is recommended to add at least some (1-2GB) swap space to keep the server running optimal in every situation. Watch what's consuming resources with [top processes by CPU](/top-processes-cpu) and [by memory](/top-processes-memory); Rocketeers can [monitor your sites and servers](/features/monitor-your-sites-and-servers) for you. [Learn how to add Swap Space to Ubuntu servers](/add-swap-space-on-ubuntu) ## Security ### How to install Certbot Source: https://rocketeersapp.com/how-to-install-certbot Certbot issues free, trusted SSL certificates from Let's Encrypt and renews them before they expire. Here is how to install it on Ubuntu, issue your first certificate, and keep it renewing on its own. A trusted SSL certificate is no longer optional — without one every visitor gets a [your connection is not private](/your-connection-is-not-private) warning, and browsers refuse to load the page. [Certbot](https://certbot.eff.org) is the official client for [Let's Encrypt](https://letsencrypt.org), which hands out free certificates that every browser trusts. The catch is that those certificates only last 90 days, so installing Certbot is really about setting up a process that renews them forever. We assume you already have a Ubuntu server with [Nginx installed](/how-to-install-nginx) and a domain pointed at it. ## Install Certbot in a virtual environment You'll find guides that install Certbot with `apt install certbot` or through snap. Both work, but the `apt` package is often stuck on an old release, and snap drags in its own runtime. The cleanest approach — and the one that gives you the DNS plugins you'll want later — is to install Certbot into its own Python virtual environment. First install Python and the tools to create the environment: ```bash DEBIAN_FRONTEND=noninteractive sudo apt-get install -y openssl python3 python3-venv ``` Create the virtual environment in `/opt/certbot` and upgrade pip inside it: ```bash sudo python3 -m venv /opt/certbot sudo /opt/certbot/bin/pip install --upgrade pip ``` Now install Certbot itself, along with the DNS provider plugins you might need: ```bash sudo /opt/certbot/bin/pip install --upgrade \ certbot \ certbot-dns-cloudflare \ certbot-dns-digitalocean \ certbot-dns-dnsimple \ certbot-dns-hetzner ``` Finally, symlink the binary onto your `PATH` so you can just type `certbot`: ```bash sudo ln -sf /opt/certbot/bin/certbot /usr/local/bin/certbot ``` Check it worked: ```bash certbot --version ``` ## Issue your first certificate There are two ways to prove to Let's Encrypt that you actually control the domain: the HTTP challenge and the DNS challenge. ### The HTTP challenge (webroot) The HTTP challenge has Let's Encrypt fetch a token from a file Certbot drops in your web root. Create the directory it serves the challenge from, then request the certificate: ```bash sudo mkdir -p /var/www/example.com/.well-known/acme-challenge sudo certbot certonly \ --webroot --webroot-path /var/www/example.com \ --preferred-challenges http \ --cert-name example.com \ --domains example.com,www.example.com \ --email you@example.com \ --rsa-key-size 4096 \ --agree-tos \ --non-interactive ``` This needs your domain's DNS already pointing at the server and port 80 reachable. ### The DNS challenge The DNS challenge proves control by creating a temporary TXT record through your DNS provider's API. It's the only option for wildcard certificates (`*.example.com`) and it works before your domain even points at the server. Store your provider credentials in a file and lock it down: ```bash sudo mkdir -p /root/.secrets echo "dns_cloudflare_api_token = your-token-here" | sudo tee /root/.secrets/cloudflare.ini sudo chmod 600 /root/.secrets/cloudflare.ini ``` Then issue the certificate using the matching plugin: ```bash sudo certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \ --cert-name example.com \ --domains example.com,*.example.com \ --email you@example.com \ --rsa-key-size 4096 \ --agree-tos \ --non-interactive ``` Either way, your certificate and private key land in `/etc/letsencrypt/live/example.com/`. ## Point Nginx at the certificate Reference the issued files in your server block, then reload Nginx: ```nginx ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ``` ```bash sudo nginx -t && sudo service nginx reload ``` Always run `nginx -t` first — reloading with a broken config can leave you with a [502 Bad Gateway](/502-bad-gateway-nginx) or worse. ## Keep it renewing automatically Let's Encrypt certificates expire after 90 days, so renewal isn't a nice-to-have — it's the whole point. Certbot can renew every certificate it manages with one command: ```bash sudo certbot renew --quiet ``` Test it without touching anything real first: ```bash sudo certbot renew --dry-run ``` To make it happen on its own, drop a cron job that renews daily and reloads Nginx afterwards so it picks up the fresh certificate: ```bash echo '0 0 * * * root /usr/local/bin/certbot renew --quiet --post-hook "service nginx reload" > /dev/null 2>&1' \ | sudo tee /etc/cron.d/certbot ``` Certbot only actually renews a certificate when it's within 30 days of expiry, so running daily is safe and gives you a wide margin if a renewal ever fails. We cover the renewal setup in more depth in [renew SSL certificates automatically](/renew-ssl-certificates-automatically). ## Confirm it's working Check what Certbot is managing and when each certificate expires: ```bash sudo certbot certificates ``` You can also verify the live certificate the way a browser sees it — see [how to check SSL certificate expiration](/check-ssl-certificate-expiration). ## Let Rocketeers handle it Installing Certbot is the easy part. The work that never ends is the part around it: storing each provider's API credentials securely, picking the right challenge per domain, wiring the renewal hook into Nginx, and noticing when a renewal silently fails three months from now. Rocketeers provisions Certbot with every DNS plugin, issues certificates over HTTP or DNS automatically, and renews them in the background for every site on every server you run — so a certificate never lapses and you never think about it again. ### How to renew SSL certificates automatically Source: https://rocketeersapp.com/renew-ssl-certificates-automatically Let's Encrypt certificates last 90 days, so renewal has to be automatic. Here is how to set up Certbot to renew every certificate and reload Nginx on its own. A [Let's Encrypt](https://letsencrypt.org) certificate is valid for 90 days. That short lifetime is deliberate — it forces automation, so a forgotten certificate can never linger and become a problem. The flip side is that renewal is not optional: if it isn't automatic, your site eventually goes dark with a [your connection is not private](/your-connection-is-not-private) warning. Here's how to make sure that never happens. This assumes you've already [installed Certbot](/how-to-install-certbot) and issued a certificate. ## How Certbot renewal works When Certbot issues a certificate it saves a renewal config under `/etc/letsencrypt/renewal/`, recording the domains, the challenge method, and the credentials it used. From then on, one command renews everything it manages: ```bash sudo certbot renew ``` The important detail: `certbot renew` only actually renews a certificate when it's within 30 days of expiry. Every other run is a no-op. That's why it's safe — and recommended — to run it far more often than the certificate's lifetime would suggest. ## Test renewal first Before trusting it, do a dry run against the staging servers. It exercises the entire renewal path without touching your real certificates or hitting rate limits: ```bash sudo certbot renew --dry-run ``` If that completes cleanly, automatic renewal will work. ## Set up the renewal cron job Add a cron entry that renews daily and reloads Nginx afterwards — the reload is what makes Nginx pick up the new certificate without dropping connections: ```bash echo '0 0 * * * root /usr/local/bin/certbot renew --quiet --post-hook "service nginx reload" > /dev/null 2>&1' \ | sudo tee /etc/cron.d/certbot ``` Breaking that down: - `0 0 * * *` runs it every day at midnight. - `--quiet` keeps it silent unless something actually happens. - `--post-hook "service nginx reload"` reloads Nginx **only** when a certificate was renewed, so it isn't reloading needlessly every day. Running daily means that even if one renewal fails, you have ~30 days of retries before the certificate actually expires. ## The systemd alternative On modern Ubuntu, Certbot often installs a `systemd` timer that does the same job. Check whether one is already active before adding a cron job: ```bash systemctl list-timers | grep certbot ``` If you see `certbot.timer`, renewal is already scheduled — you only need to make sure a reload hook is configured. Use one mechanism or the other, not both. ## Confirm what's scheduled to renew List every certificate Certbot manages and its expiry date: ```bash sudo certbot certificates ``` For peace of mind, [check the expiry of the live certificate](/check-ssl-certificate-expiration) the way a browser sees it, and consider a monitoring alert so you hear about a stalled renewal long before visitors do. ## Let Rocketeers handle it Automatic renewal is a chain — the cron job has to exist, the reload hook has to fire, the DNS or HTTP challenge has to keep working, and someone has to notice if a renewal quietly fails three months from now. Rocketeers renews every certificate across every server it manages and reloads the web server for you, and it surfaces any failure so a certificate never silently lapses. If you'd rather not run certificates on the origin at all, you can also [terminate SSL at Cloudflare](/a-plus-grade-ssl-using-cloudflare). ### How to configure rate limiting in nginx Source: https://rocketeersapp.com/nginx-rate-limiting Rate limiting protects login pages, APIs, and expensive endpoints from abuse and brute-force attacks. nginx does this with the built-in limit_req module. Here is how to configure it. ## How nginx rate limiting works nginx rate limiting has two parts: you define a **zone** that tracks request rates per client in the `http` block, then **apply** that zone to the locations you want to protect. The `limit_req` module uses a leaky-bucket algorithm, so a steady rate is allowed while bursts are smoothed out or rejected. ## Define a rate-limit zone Add this inside the `http` block (in `nginx.conf`): ``` limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s; ``` - `$binary_remote_addr` keys the limit on the client IP address (compact form). - `zone=mylimit:10m` names the zone and reserves 10 MB of shared memory — enough for roughly 160,000 IPs. - `rate=10r/s` allows 10 requests per second per IP. Use `r/m` for per-minute limits on sensitive endpoints. ## Apply the limit to a location Reference the zone inside a `server` or `location` block: ``` location /login { limit_req zone=mylimit burst=20 nodelay; # ... your usual proxy_pass or fastcgi_pass } ``` - `burst=20` lets a short spike of up to 20 queued requests through instead of rejecting them immediately. - `nodelay` serves those burst requests right away rather than spacing them out — usually what you want for web traffic. ## Return 429 instead of 503 By default nginx rejects limited requests with `503 Service Unavailable`. `429 Too Many Requests` is more accurate and friendlier to API clients: ``` limit_req_status 429; ``` ## Protect a login endpoint against brute force A tight per-minute limit is ideal for login and password-reset pages: ``` limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; location = /login { limit_req zone=login burst=3 nodelay; limit_req_status 429; # ... } ``` ## Apply and test Check the configuration before reloading so a typo doesn't take the site down: ```bash sudo nginx -t sudo systemctl reload nginx ``` You can confirm it's working by hammering the endpoint and watching for `429` responses: ```bash for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code}\n" https://example.com/login; done ``` Rate limiting pairs well with other server-level protections such as a [Content Security Policy in nginx](/content-security-policy) and the broader checklist in [optimizing web application security](/optimize-web-application-security). ### Convert SSL certificate formats with OpenSSL Source: https://rocketeersapp.com/convert-ssl-certificate-formats Certificates come in PEM, CRT, CER, DER, and PFX formats, and software is picky about which one it wants. Here are the openssl commands to convert between all of them. ## The formats, quickly Before converting, it helps to know what you actually have: - **PEM** — Base64 text, starts with `-----BEGIN CERTIFICATE-----`. The most common format on Linux. Can hold a certificate, a key, or a whole chain. - **CRT / CER** — usually just a PEM (or DER) certificate with a different file extension. - **DER** — the binary version of a PEM certificate. Common in the Windows and Java world. - **PFX / P12** — a binary, password-protected bundle holding the certificate **and** its private key together. Standard on Windows / IIS. Everything below uses the `openssl` command line tool. ## PEM and CRT These are typically the same format, so converting is often just a rename. To be safe, normalise to PEM: ```bash openssl x509 -in certificate.crt -out certificate.pem -outform PEM ``` ## PEM to DER ```bash openssl x509 -in certificate.pem -outform DER -out certificate.der ``` ## DER to PEM ```bash openssl x509 -in certificate.der -inform DER -out certificate.pem -outform PEM ``` ## PFX to PEM (certificate and key) A PFX bundles both parts. To pull them out into a single PEM file: ```bash openssl pkcs12 -in certificate.pfx -out certificate.pem -nodes ``` If you only want one part, see the dedicated guides for [extracting the certificate from a PFX file](/extract-certificate-from-pfx-file) and [extracting the private key from a PFX file](/extract-private-key-from-pfx-file). ## PEM/CRT and key to PFX Going the other way — bundling a certificate and its private key into a PFX for Windows or IIS: ```bash openssl pkcs12 -export -out certificate.pfx \ -inkey private.key -in certificate.crt ``` Add the intermediate certificate so the bundle carries the full chain: ```bash openssl pkcs12 -export -out certificate.pfx \ -inkey private.key -in certificate.crt -certfile intermediate.crt ``` You'll be prompted for an export password, which whoever imports the PFX will need. ## Verify the result After any conversion, confirm the certificate is readable and contains what you expect: ```bash openssl x509 -in certificate.pem -noout -text ``` If your goal is a working HTTPS setup, make sure the certificate you serve includes the intermediates — see [what is an SSL certificate chain](/what-is-an-ssl-certificate-chain). ### How to generate an SSH key Source: https://rocketeersapp.com/generate-ssh-key Here is how to generate an SSH key pair on Linux, macOS, or Windows and add the public key to a server, GitHub, or GitLab. If you're not sure what an SSH key is or how it works, start with [what is an SSH key](/what-is-an-ssh-key). Otherwise, here's how to make one. ## Generate the key pair Use `ssh-keygen` with the modern ed25519 algorithm. The comment (`-C`) is just a label to help you recognise the key later: ```bash ssh-keygen -t ed25519 -C "you@example.com" ``` You'll be asked where to save the key (press Enter for the default `~/.ssh/id_ed25519`) and for an optional passphrase. A passphrase encrypts your private key on disk — recommended for laptops. If you need to support older systems that don't speak ed25519, generate an RSA key instead: ```bash ssh-keygen -t rsa -b 4096 -C "you@example.com" ``` ## Where the files end up The command creates two files in `~/.ssh`: - `id_ed25519` — your **private** key. Never share or copy this off your machine. - `id_ed25519.pub` — your **public** key. This is the one you hand out. ## Add the key to the ssh-agent The agent keeps your unlocked key in memory so you don't retype the passphrase every time: ```bash eval "$(ssh-agent -s)" ssh-add ~/.ssh/id_ed25519 ``` ## Copy the public key to a server The easiest way to install your public key on a server is `ssh-copy-id`: ```bash ssh-copy-id user@your-server ``` This appends your public key to the server's `~/.ssh/authorized_keys`. From then on you can log in without a password: ```bash ssh user@your-server ``` If `ssh-copy-id` isn't available, print the public key and paste it into `~/.ssh/authorized_keys` on the server yourself: ```bash cat ~/.ssh/id_ed25519.pub ``` ## Add the key to GitHub or GitLab Copy the public key to your clipboard, then paste it into **Settings → SSH and GPG keys** (GitHub) or **Preferences → SSH Keys** (GitLab): ```bash # macOS pbcopy < ~/.ssh/id_ed25519.pub # Linux (X11) xclip -sel clip < ~/.ssh/id_ed25519.pub ``` Test the connection afterwards: ```bash ssh -T git@github.com ``` ## If the connection is rejected If the server or service refuses your key, the fixes are in [SSH Permission denied (publickey)](/ssh-permission-denied-publickey) and, for GitHub specifically, [GitHub Permission denied (publickey)](/github-permission-denied-publickey). ### How to generate a CSR with OpenSSL Source: https://rocketeersapp.com/generate-csr-with-openssl A CSR (Certificate Signing Request) is the file you send to a certificate authority to request an SSL certificate. Here is how to generate one, and its private key, with openssl. ## What a CSR contains A Certificate Signing Request bundles the details of the certificate you want — your domain name and organisation info — and your **public** key, all signed by your **private** key. The CA uses it to issue a certificate. The private key it's generated alongside stays with you and is never sent to the CA. ## Generate a private key and CSR This single command creates a new 2048-bit private key and a matching CSR: ```bash openssl req -new -newkey rsa:2048 -nodes \ -keyout domain.key -out domain.csr ``` You'll be prompted for the certificate details. The important one is **Common Name** — it must be the exact domain you're securing, for example `www.example.com`. `-nodes` leaves the private key unencrypted, which is what web servers expect. Two files result: - `domain.key` — your private key. Keep it safe; you'll need it to install the certificate. - `domain.csr` — the request to send to your CA. ## Generate a CSR with Subject Alternative Names (SAN) Modern certificates should list every hostname under SAN, not just the Common Name. Pass them inline: ```bash openssl req -new -newkey rsa:2048 -nodes \ -keyout domain.key -out domain.csr \ -subj "/CN=example.com" \ -addext "subjectAltName=DNS:example.com,DNS:www.example.com" ``` ## Generate a CSR from an existing key If you already have a private key and just need a new request (for a renewal, say): ```bash openssl req -new -key domain.key -out domain.csr ``` ## Verify the CSR before sending it Always check the request decodes correctly and lists the right names: ```bash openssl req -in domain.csr -noout -text ``` Once your CA returns the signed certificate, you may need to [convert it to another format](/convert-ssl-certificate-formats) and make sure you serve the [complete certificate chain](/what-is-an-ssl-certificate-chain). For local development where you don't need a CA at all, generate a [self-signed certificate](/generate-self-signed-certificate-openssl) instead. ### How to change the SSH port on Ubuntu Source: https://rocketeersapp.com/change-ssh-port-ubuntu Moving SSH off the default port 22 cuts down on automated brute-force noise in your logs. Here is how to do it safely on Ubuntu, including the socket-activation change that catches people out on 24.04, without locking yourself out. Changing the port is not real security on its own — see [optimizing web application and server security](/optimize-web-application-security) for the controls that matter — but it dramatically reduces the volume of drive-by login attempts. Here's how to do it without losing access to your server. (For what the default port is and why it matters, see [the SSH port explained](/ssh-port).) ## Set the new port in sshd_config Edit the SSH daemon config: ```bash sudo nano /etc/ssh/sshd_config ``` Find the `Port` line, uncomment it, and set your chosen port (pick something above 1024, for example 2222): ``` Port 2222 ``` ## Ubuntu 22.10 and newer: update the socket too This is the step most guides miss. On Ubuntu 22.10, 24.04, and later, `ssh` is **socket-activated** by systemd, so the `Port` in `sshd_config` is ignored — the listening port is defined in `ssh.socket` instead. Check whether socket activation is in use: ```bash sudo systemctl status ssh.socket ``` If it's active, override the socket's port: ```bash sudo systemctl edit ssh.socket ``` Add these lines in the editor (the empty `ListenStream=` clears the default of 22): ``` [Socket] ListenStream= ListenStream=2222 ``` On older releases (Ubuntu 22.04 and earlier) there's no socket — the `sshd_config` change alone is enough. ## Open the new port in the firewall If you run UFW, allow the new port **before** restarting, or you'll lock yourself out: ```bash sudo ufw allow 2222/tcp ``` ## Apply the change Reload systemd and restart SSH: ```bash sudo systemctl daemon-reload sudo systemctl restart ssh.socket ssh ``` ## Test in a new session — don't close the old one Keep your current SSH session open. From another terminal, connect on the new port: ```bash ssh -p 2222 user@your-server ``` Only once that succeeds should you close the original session. If the new connection is refused or rejected, work through [SSH Permission denied (publickey)](/ssh-permission-denied-publickey) and double-check the firewall rule. ## Clean up the firewall After confirming the new port works, remove the old rule if you'd added one for port 22: ```bash sudo ufw delete allow 22/tcp ``` ### What is an SSH key Source: https://rocketeersapp.com/what-is-an-ssh-key An SSH key is a pair of cryptographic keys used to log in to servers and services without a password. The public key lives on the server, the private key stays on your machine, and only the two together grant access. ## How SSH keys work An SSH key comes as a **pair**: a private key and a public key. They are generated together and are mathematically linked. - The **private key** stays on your computer and is never shared. Treat it like a password. - The **public key** is copied to any server or service you want to access. When you connect, the server uses your public key to issue a challenge that only the matching private key can answer. Your private key never leaves your machine, and no secret is sent over the network. If the answer checks out, you're in. ## Why use keys instead of passwords - **More secure** — a 256-bit key is effectively impossible to brute-force, unlike a typed password. - **No password prompts** — once set up, connections are automatic, which makes scripting and deployments painless. - **Easy to revoke** — remove one public key from a server to cut off one machine, without changing anything else. This is why password authentication is often disabled entirely on hardened servers. See [optimizing web application and server security](/optimize-web-application-security) for the bigger picture. ## Key types When you generate a key you choose an algorithm. In 2026 the recommendation is simple: - **ed25519** — fast, secure, and short. Use this unless you have a specific reason not to. - **rsa** — still fine at 4096 bits, and the most widely compatible with older systems. - **ecdsa** — supported, but ed25519 is the better modern choice. ## Where SSH keys live On your machine, keys are stored in `~/.ssh`: - `~/.ssh/id_ed25519` — your private key. - `~/.ssh/id_ed25519.pub` — your public key. On a server, the public keys that are allowed to log in to an account are listed in that account's `~/.ssh/authorized_keys` file. ## Generate a key Creating a key takes one command: ```bash ssh-keygen -t ed25519 -C "you@example.com" ``` For the full walkthrough — including copying the key to a server and adding it to GitHub — see [how to generate an SSH key](/generate-ssh-key). If the server rejects your key when you connect, the cause is almost always covered in [SSH Permission denied (publickey)](/ssh-permission-denied-publickey). ### How to check SSL certificate expiration Source: https://rocketeersapp.com/check-ssl-certificate-expiration Here is how to check when an SSL certificate expires, both for a live website and for a certificate file on disk, using openssl from the command line. An expired certificate takes a site down hard — every visitor sees [your connection is not private](/your-connection-is-not-private). Checking the expiry date takes one command. You can also run the same check in your browser with our free [SSL checker](/ssl). ## Check a live website Connect to the site and read the validity dates straight from the certificate it serves: ```bash echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \ | openssl x509 -noout -dates ``` You'll get the start and end of the validity window: ```bash notBefore=Mar 1 00:00:00 2026 GMT notAfter=May 30 23:59:59 2026 GMT ``` To see only the expiry date, swap `-dates` for `-enddate`: ```bash echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \ | openssl x509 -noout -enddate ``` ## Check a certificate file on disk If you have the certificate as a local file: ```bash openssl x509 -in certificate.pem -noout -enddate ``` This works on any PEM or CRT file. For other formats, see [converting certificate formats](/convert-ssl-certificate-formats) first. ## Check whether it expires within N days The `-checkend` flag is built for scripts and monitoring. It takes a number of seconds and exits non-zero if the certificate expires within that window. This checks for the next 30 days (30 × 86400 = 2592000 seconds): ```bash openssl x509 -in certificate.pem -noout -checkend 2592000 ``` ```bash echo $? # 0 = still valid, 1 = expires within 30 days ``` Wire that exit code into a cron job or a monitoring check and you'll get warned before a certificate ever lapses. If your certificate is valid but the connection still fails, the cause is often a [missing certificate chain](/what-is-an-ssl-certificate-chain) rather than the expiry date. ### How to generate a self-signed certificate with OpenSSL Source: https://rocketeersapp.com/generate-self-signed-certificate-openssl A self-signed certificate lets you serve HTTPS for local development or internal services without going through a certificate authority. Here is how to create one with openssl. ## When to use a self-signed certificate A self-signed certificate is signed by its own key rather than a trusted CA. That's perfect for **local development, testing, and internal services** where you control the clients. It is **not** suitable for a public website — browsers don't trust it and will show [your connection is not private](/your-connection-is-not-private) to every visitor. ## Generate a certificate and key in one command This creates a private key and a self-signed certificate valid for one year: ```bash openssl req -x509 -newkey rsa:2048 -nodes \ -keyout key.pem -out cert.pem -days 365 \ -subj "/CN=localhost" ``` - `cert.pem` — the certificate. - `key.pem` — the private key. `-nodes` keeps the key unencrypted so a web server can read it without a passphrase. ## Include Subject Alternative Names Modern browsers ignore the Common Name and require the hostname under **SAN**, or they'll reject the certificate outright. Add it explicitly: ```bash openssl req -x509 -newkey rsa:2048 -nodes \ -keyout key.pem -out cert.pem -days 365 \ -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" ``` ## Use it in nginx Point your server block at the two files: ``` ssl_certificate /etc/ssl/cert.pem; ssl_certificate_key /etc/ssl/key.pem; ``` ## Trust it locally Your browser will still warn you because nothing vouches for the certificate. For a smoother local setup you can add `cert.pem` to your operating system or browser trust store, or use a tool like `mkcert` that installs a local CA for you. When you're ready to serve real traffic, request a certificate from a CA with a [CSR](/generate-csr-with-openssl), or use a free automated certificate by [installing Certbot](/how-to-install-certbot). Either way, make sure you serve the [full certificate chain](/what-is-an-ssl-certificate-chain). ### What is an SSL certificate chain Source: https://rocketeersapp.com/what-is-an-ssl-certificate-chain An SSL certificate chain links your website's certificate back to a trusted root certificate. When a link in that chain is missing, browsers and command line tools reject the connection even though the certificate itself is perfectly valid. ## What is a certificate chain When a browser connects to your website over HTTPS, it doesn't trust your certificate directly. Instead it follows a **chain of trust** from your certificate up to a root certificate it already trusts. Each certificate in the chain is signed by the one above it, and the root is pre-installed in the browser or operating system trust store. If the browser can build an unbroken path from your certificate to a trusted root, the connection is secure. If it can't, it shows a warning. ## The three links in the chain A complete chain has three types of certificate: - **Root certificate** — owned by the Certificate Authority (CA) and shipped inside every browser and operating system. It is never sent over the wire; the client already has it. - **Intermediate certificate(s)** — signed by the root, used by the CA to issue your certificate. There can be more than one. - **Leaf certificate** — also called the server or end-entity certificate. This is the one issued for your domain. The browser ships the root. **Your server must send the leaf certificate plus every intermediate certificate** so the browser can connect the two ends. ## Why an incomplete chain breaks HTTPS The most common SSL mistake is installing only the leaf certificate and forgetting the intermediates. It often looks fine in your own browser (which may have cached the intermediate from another site) but fails for other visitors and for tools like `curl`. A broken or incomplete chain typically shows up as: - [curl (60) SSL certificate problem: unable to get local issuer certificate](/curl-60-ssl-certificate-problem-unable-to-get-local-issuer-certificate) - [NET::ERR_CERT_AUTHORITY_INVALID](/net-err-cert-authority-invalid) - [Your connection is not private](/your-connection-is-not-private) ## Inspect the chain a server is sending Use `openssl` to see exactly which certificates your server presents: ```bash openssl s_client -connect example.com:443 -servername example.com -showcerts ``` Each `-----BEGIN CERTIFICATE-----` block is one certificate in the chain. You should see your leaf certificate followed by one or more intermediates. If you only see one certificate, your chain is incomplete. ## Fix an incomplete chain The fix is to serve the **full chain**: your leaf certificate followed by the intermediate certificate(s), in order, in a single file. Concatenate them leaf-first: ```bash cat domain.crt intermediate.crt > fullchain.crt ``` Then point your web server at the combined file. In nginx: ``` ssl_certificate /etc/ssl/fullchain.crt; ssl_certificate_key /etc/ssl/domain.key; ``` If you use Let's Encrypt, this is already done for you — always point `ssl_certificate` at `fullchain.pem`, not `cert.pem`. After reloading the server, re-run the `openssl s_client` command above and confirm the full chain is now sent. Once the chain is complete, you can verify your overall configuration with the [SSLLabs test and aim for an A+ grade](/a-plus-grade-ssl-using-cloudflare). ### How to setup OpenClaw securely on your own VPS Source: https://rocketeersapp.com/setup-openclaw-vps-securely OpenClaw is a powerful open-source AI assistant that runs on your own infrastructure. Learn how to deploy it securely on a VPS to avoid common security pitfalls. ## What is OpenClaw? [OpenClaw](https://openclaw.ai) is an open-source AI assistant that runs on your own infrastructure, giving you complete control over your data and deployment. It can connect to various messaging platforms including WhatsApp, Slack, Discord, Google Chat, Signal, and iMessage. OpenClaw can control browsers, generate videos and images, and even run scheduled tasks via cron. Unlike cloud-based AI services, OpenClaw runs entirely on your server, making it an attractive option for privacy-conscious users and organizations that need to keep their data within their own infrastructure. For more details, check the [official OpenClaw documentation](https://docs.openclaw.ai/). ## Why security matters for OpenClaw A recent Shodan scan revealed a concerning security landscape: **42,665 OpenClaw instances were found exposed to the public internet**, with 93.4% having authentication bypasses. Even more alarming, eight instances were completely open with no password, no token, and full shell access available to anyone who connected. This makes security configuration absolutely critical when deploying OpenClaw. An improperly secured instance could give attackers complete access to your server and all connected services. ## Choosing a VPS provider Several VPS providers offer streamlined OpenClaw deployment with varying levels of built-in security: **Hetzner** Hetzner offers excellent value with double the vCPUs and RAM at a fraction of the cost compared to other providers. However, like Hostinger, you'll need to implement all security measures yourself. **DigitalOcean (Recommended for beginners)** DigitalOcean offers a [1-Click Deploy](https://www.digitalocean.com/community/tutorials/how-to-run-openclaw) specifically designed for OpenClaw. This deployment automatically implements security best practices including: - Authenticated communication via gateway tokens - Hardened firewall rules that rate-limit OpenClaw ports - Private DM pairing by default The 1-Click Deploy handles much of the security configuration automatically, making it ideal for users who want a secure setup without extensive manual configuration. ## Essential security measures Regardless of which provider you choose, implement these critical security measures: **1. Use SSH key-based authentication** Never use password authentication for SSH access. Generate and use SSH keys instead: ```bash # On your local machine, generate an SSH key ssh-keygen -t ed25519 -C "your_email@example.com" # Copy the public key to your VPS ssh-copy-id user@your-vps-ip ``` After confirming key-based authentication works, disable password authentication in `/etc/ssh/sshd_config`: ```bash PasswordAuthentication no PubkeyAuthentication yes ``` **2. Keep the gateway on loopback** The OpenClaw gateway should never be directly exposed to the internet. Configure it to listen only on localhost (127.0.0.1) and access it through an SSH tunnel or Tailscale. **Access via SSH tunnel:** ```bash ssh -L 8080:localhost:8080 user@your-vps-ip ``` Then access OpenClaw locally at `http://localhost:8080` **Access via Tailscale:** Install [Tailscale](https://tailscale.com/) on your VPS and enable Tailscale Serve to securely expose OpenClaw only to devices on your private Tailscale network. ```bash # Install Tailscale curl -fsSL https://tailscale.com/install.sh | sh # Authenticate and connect sudo tailscale up # Serve OpenClaw securely tailscale serve https / http://127.0.0.1:8080 ``` **3. Require gateway authentication** If you must bind OpenClaw to your LAN or Tailscale network, always require authentication. OpenClaw supports two authentication methods: **Using a gateway token (recommended):** ```bash # Set in your OpenClaw configuration OPENCLAW_GATEWAY_TOKEN=your-secure-random-token-here ``` Generate a strong, random token and store it securely. You'll need this token to access the OpenClaw web interface. **Using a password:** ```bash # Alternative authentication method OPENCLAW_GATEWAY_PASSWORD=your-strong-password-here ``` **4. Configure firewall rules** Set up a firewall to restrict access to essential ports only: ```bash # Install UFW (Uncomplicated Firewall) sudo apt install ufw # Deny all incoming traffic by default sudo ufw default deny incoming sudo ufw default allow outgoing # Allow SSH (change 22 if using a custom port) sudo ufw allow 22/tcp # Enable the firewall sudo ufw enable # Check status sudo ufw status ``` Do not open ports for OpenClaw gateway access. Instead, use SSH tunneling or Tailscale as described above. **5. Keep software updated** Regularly update both your system packages and OpenClaw itself: ```bash # Update system packages sudo apt update && sudo apt upgrade -y # Update OpenClaw (run inside your OpenClaw directory) git pull origin main docker compose down docker compose build --no-cache docker compose up -d # Check the latest release at: https://github.com/openclaw/openclaw/releases ``` ## Installation and deployment **Quick setup with Docker** The easiest way to deploy OpenClaw is using Docker. Here's a secure setup process: ```bash # Update system sudo apt update && sudo apt upgrade -y # Install Docker curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh # Clone OpenClaw repository git clone https://github.com/openclaw/openclaw.git cd openclaw # Create a secure .env file cp .env.example .env nano .env ``` In your `.env` file, configure these critical settings: ```bash # Generate a strong random token OPENCLAW_GATEWAY_TOKEN=$(openssl rand -hex 32) # Bind only to localhost OPENCLAW_GATEWAY_HOST=127.0.0.1 OPENCLAW_GATEWAY_PORT=8080 # Set your AI provider API keys securely OPENAI_API_KEY=your-key-here ANTHROPIC_API_KEY=your-key-here ``` Deploy OpenClaw: ```bash # Build and start containers docker compose build --no-cache docker compose up -d # Check logs docker compose logs -f ``` **Securing API credentials** Store all API credentials (OpenAI, Anthropic, etc.) in environment variables, never in code or configuration files that might be committed to version control. Create a separate `.env` file that's excluded from git: ```bash # Ensure .env is in .gitignore echo ".env" >> .gitignore ``` Set appropriate file permissions: ```bash chmod 600 .env ``` ## Post-installation security checks After installation, verify your security configuration: **1. Check exposed ports** ```bash sudo netstat -tulpn | grep LISTEN ``` You should only see SSH (port 22) and Docker internal services. The OpenClaw gateway should be listening on 127.0.0.1 only, not 0.0.0.0. **2. Test authentication** Try accessing the gateway without authentication to ensure it's properly protected: ```bash curl http://localhost:8080 ``` This should return an authentication error if properly configured. **3. Review Docker container security** ```bash # Check running containers docker ps # Review container logs for errors docker compose logs --tail=100 ``` ## Maintenance and monitoring **Regular security audits** Schedule monthly security reviews: - Check for unauthorized SSH access attempts: `sudo grep "Failed password" /var/log/auth.log` - Review OpenClaw access logs for suspicious activity - Update all dependencies and Docker images - Verify firewall rules remain intact **Automated backups** Set up automated backups of your OpenClaw configuration and data: ```bash # Create a backup script cat > /root/backup-openclaw.sh << 'EOF' #!/bin/bash BACKUP_DIR="/backups/openclaw" DATE=$(date +%Y%m%d_%H%M%S) mkdir -p $BACKUP_DIR cd /path/to/openclaw # Backup configuration and data tar -czf $BACKUP_DIR/openclaw_backup_$DATE.tar.gz \ .env \ docker-compose.yml \ data/ # Keep only last 30 days of backups find $BACKUP_DIR -name "openclaw_backup_*.tar.gz" -mtime +30 -delete EOF chmod +x /root/backup-openclaw.sh # Schedule daily backups via cron (crontab -l 2>/dev/null; echo "0 2 * * * /root/backup-openclaw.sh") | crontab - ``` ## Common security mistakes to avoid 1. **Exposing the gateway to 0.0.0.0** - Always bind to 127.0.0.1 and use tunneling 2. **Using weak or no authentication tokens** - Generate strong random tokens 3. **Running as root** - Create a dedicated user for OpenClaw 4. **Not updating regularly** - Set up automatic security updates 5. **Storing credentials in git** - Use environment variables and secure .env files 6. **Opening unnecessary firewall ports** - Only expose SSH, use tunnels for everything else 7. **Using default passwords** - Change all default credentials immediately 8. **Not monitoring logs** - Set up log monitoring and alerting ## Conclusion OpenClaw is a powerful tool that requires careful security configuration. By following the practices outlined in this guide, you can deploy OpenClaw securely and avoid becoming part of the statistics of exposed instances. Remember: security is not a one-time setup but an ongoing process. Regularly review your configuration, keep software updated, and monitor for suspicious activity. The key principles are: - Never expose OpenClaw directly to the internet - Always require strong authentication - Use SSH tunneling or Tailscale for access - Keep software updated - Monitor and audit regularly With these measures in place, you can confidently run OpenClaw on your VPS while maintaining robust security. ### How to get A+ grade SSL using Cloudflare Source: https://rocketeersapp.com/a-plus-grade-ssl-using-cloudflare By default Cloudflare configures your security for SSL and HTTPS traffic for maximum connectivity and not for best security. Connectivity and security are unexchangeable, by letting more old insecure clients connect, you lower the bar for all clients that connect to your website. To keep everything as secure as possible, it is advised to make use of the best new practices and to let go of old and crumbling technology. On the web security is always improving and therefore shifting away from older technologies that just don't make the cut anymore. To analyze your HTTPS connection for your website, the golden standard for SSL configuration is the [SSL Server Test from SSLLabs](https://globalsign.ssllabs.com). This test gives your security configuration a grade and shows you if there are areas for improvement. The highest grade is an A+ and to achieve this using Cloudflare, follow these easy steps: ## Getting started Login to [Cloudflare](/providers/cloudflare) and navigate to the domain which you want to improve the SSL configuration for. Navigate to the "SSL/TLS" section and then click on the "Edge Certificates" submenu. ## Enable Always Use HTTPS Enable HTTPS for every visitor by enabling the "Always Use HTTPS" option. ## Enable HSTS To make sure a browser can't connect using HTTP anymore and go directly into HTTPS mode, choose "Enable HSTS" to configure HTTP Strict Transport Security (HSTS)". Acknowledge the notice that once this setting is enabled, you can't easily go back. So if your website or server does still need HTTP (yikes!) for some legacy URL, you have a problem. But the HTTP URL is already a problem on itself. ## Set minimum TLS version to 1.2 Scroll down until you see "Minimum TLS Version" and select "1.2" as the minimum version clients can use to connect to your website. This skips unsecure versions 1.0 and 1.1 of TLS. At some point it should be feasible to add 1.3 as the minimum version, but nowadays there are too many clients still compatible with 1.2 only. ## Make sure to enable "TLS 1.3" Enable TLS 1.3 by making sure the toggle is switched on, this way the newest version of TLS can be used by all clients. ## Fix legacy HTTP URLs automatically This one is not necessarily needed for an A+ grade on SSLLabs, but enabling "Automatic HTTPS Rewrites" makes sure your websites does not follow or uses any HTTP references anymore. Everything should be HTTPS, to make sure no mixed content is used on your website. You're done! Now run that SSLLabs test and get your A+ grade. For a quick second opinion you can also run our free [SSL checker](/ssl) and [HSTS checker](/hsts) against your domain. ## Common SSL errors after changing settings If something breaks after tightening your SSL, a few errors come up again and again. Setting the Cloudflare SSL mode incorrectly (Flexible instead of Full) is the classic cause of [ERR_TOO_MANY_REDIRECTS](/err-too-many-redirects). A visitor-facing warning like [your connection is not private](/your-connection-is-not-private) or [NET::ERR_CERT_AUTHORITY_INVALID](/net-err-cert-authority-invalid) usually points at an expired certificate or a missing certificate chain. ### How to optimize web application security Source: https://rocketeersapp.com/optimize-web-application-security There are a lot of features you can use to increase overall security of your website or web app. ## HTTPS The web server can serve your web application over a secured HTTPS connection using a signed SSL certificate. In the earlier days of the internet is wasn't that common like it is today. Make sure your redirecting all your normal HTTP traffic to HTTPS by default, so that every request to your web application gets automatically upgraded and secure. In addition, [HSTS](#http-strict-transport-security-hsts) can help with this. ## HSTS The HTTP Strict-Transport-Security (HSTS) response header informs the browser that the site should only be accessed using HTTPS, and that any future attempts to access it using HTTP should automatically be converted to HTTPS. This setting can be defined at server and web application level using HTTP headers. The setting can be cached heavily by the browser and therefore make your website inaccessible when the HTTPS connection is not configured correctly. ## CSRF Cross-Site Request Forgery (CSRF) is a protection mechanism for preventing requests being made from outside your application. Typically it is used to prevent all non-GET requests. For possibly destructive actions like POST, PUT and DELETE there is a verification performed by your application that checks a randomly generated token that is attached to the session of the user. Because only the server can retrieve the token correctly and send it with any non-GET request, then the application can be sure that the request is coming from the application itself. ## CORS Cross-Origin Resource Sharing (CORS) is a protection layer configured in your web server or web application using an HTTP-header that defines an origin other than its own to permit the browser to load resources from. By default browsers block loading resources from external domains using Javascript. [Read more about CORS on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) ## DNSSEC Domain Name System Security Extensions (DNSSEC) is a protection layer on top of DNS. It makes sure that the DNS server that is responding to the HTTP client (for example a web browser) is authenticated by the domain authority that keeps a register of the domain in the registry. Therefore the DNSSEC needs to be implemented on the domain registry level and contains settings that need to match the settings in the DNS. [Read more about DNSSEC on SIDN](https://www.sidn.nl/en/modern-internet-standards/dnssec) ## DANE DNS-based Authentication of Named Entities (DANE) is a protocol that only works when DNSSEC is activated. ## CSP [Content Security Policy (CSP)](/content-security-policy) is a protection layer configured in your web server or web application using an HTTP-header that defines what resources are allowed to be loaded by the browser. This can be used to prevent loading resources from external domains or to prevent loading resources that are not using HTTPS. ## Nonce A nonce is a randomly generated token that should be used only one time for one request to your web application and can be used by Content-Security Policy (CSP). The nonce can be generated by the web server or the web application and is being sent as HTTP header and inside the HTML. By adding a `nonce` attribute to `